How to add download feature as a sub-section of a function(def)

Hi, I have 2 button widgets, first one invokes a function which does the work of getting the data i need and the second button has this code which just downloads the data (from above step).

dwlbutton.js_on_click(CustomJS(args=dict(source=source), code=open(join(dirname(file), “download.js”)).read()))

The code for the first button is:

genbutton.on_click(preparedata)

I am now trying to see how to combine these buttons into a single one so that user doesn’t have to click 2 buttons. I would like a single button to prepare the data and also download.

I tried copying below code to the end of the funtion preparedata but it didn’t work.

CustomJS(args=dict(source=source), code=open(join(dirname(file), “download.js”)).read())

Any ideas on how to combine the functionality into one would help.

cheers.

There is not any direct way to invoke CustomJS callbacks from Python. Any solution will be indirect, e.g. making a DataModel [1] just to attach a js_on_change to and then changing the property value to trigger the JS callback to run.


  1. In the past this would often have been done with some property on an invisible glyph ↩︎

Thanks for the quick response, @Bryan

@Bryan I tried using a dummy select (dummysel) and tried changing the value but it didn’t quite work, may be I am doing it wrong. When the first download button is clicked it calls a function which changes the value of this select.

dummysel.value='y' # change from initially set ‘n’ to ‘y’

Any suggestions on how to make this right? Here is the on_change statement

dummysel.js_on_change("value",CustomJS(args=dict(source=source),
         code=open(join(dirname(__file__), "download.js")).read()))

Any suggestion on what type of dummy glyph to use would be helpful.

What does “didn’t quite work” actually mean? Can you provide a Minimal Reproducible Example?

Sorry for not being very clear. Basically I am not seeing ‘any action happening’. The ‘select’ value gets changed but the download doesn’t happen.

My code is quite large, so I created a small snippet which has the same concept i am trying to get to work. Hope this helps:

from bokeh.models import ColumnDataSource, CustomJS
from os.path import dirname, join

dummysel = Select(value='n', options=['y', 'n'])

df=pd.DataFrame()
source = ColumnDataSource(df)

def upd():
    global df, source #not a great practice.. but still..
    data = {
        'A': ['A1', 'A2', 'A3', 'A4', 'A5'],
        'B': ['B1', 'B2', 'B3', 'B4', 'B4'],
        'C': ['C1', 'C2', 'C3', 'C3', 'C3'],
        'D': ['D1', 'D2', 'D2', 'D2', 'D2'],
        'E': ['E1', 'E1', 'E1', 'E1', 'E1']}

    # Convert the dictionary into DataFrame
    df = pd.DataFrame(data)
    cds = ColumnDataSource(df)
    source.data = dict(cds.data)
    print(dummysel.value)
    dummysel.value = 'y' # change to value to trigger the download
    print(dummysel.value) # correctly shows changed value (from n to y) but download doesn't happen
    print(source.data)

dlbutton = Button(label="print", button_type="success",width=100)

dummysel.js_on_change('value',CustomJS(args=dict(source=source),
                                   code=open(join(dirname(__file__), "download.js")).read()))

dlbutton.on_click(upd)

curdoc().add_root(dlbutton)

you can put a console.log() inside the download.js file to see if the JS code is executed. It would be nice to see the code in download.js

That’s very likely where the problem is, and the most important thing to provide.

Here it is @Bryan @nghenzi

function table_to_csv(source) {
    const columns = Object.keys(source.data)
    const nrows = source.get_length()
    const lines = [columns.join(',')]

    for (let i = 0; i < nrows; i++) {
        let row = [];
        for (let j = 0; j < columns.length; j++) {
            const column = columns[j]
            row.push(source.data[column][i].toString())
        }
        lines.push(row.join(','))
    }
    return lines.join('\n').concat('\n')
}


const filename = 'data_result.csv'
const filetext = table_to_csv(source)
const blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' })

//addresses IE
if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, filename)
} else {
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.download = filename
    link.target = '_blank'
    link.style.visibility = 'hidden'
    link.dispatchEvent(new MouseEvent('click'))
}

As side note, you get more help if the code is ready to be copied, pasted and run without modification.

Going to the point, there was 2 problems, in the upd function it was missing an event argunment and in the JS code the \n needs two “\”. Below you can finde the whole code and a video showing how it works.

import pandas as pd
from bokeh.layouts import column
from bokeh.models import (ColumnDataSource, CustomJS, 
    Select, Button)
from bokeh.plotting import curdoc
from os.path import dirname, join

dummysel = Select(value='n', options=['y', 'n'])

df = pd.DataFrame()
global source
source = ColumnDataSource(df)

def upd(e):
    global df, source 
    data = {
        'A': ['A1', 'A2', 'A3', 'A4', 'A5'],
        'B': ['B1', 'B2', 'B3', 'B4', 'B4'],
        'C': ['C1', 'C2', 'C3', 'C3', 'C3'],
        'D': ['D1', 'D2', 'D2', 'D2', 'D2'],
        'E': ['E1', 'E1', 'E1', 'E1', 'E1']
    }

    df = pd.DataFrame(data)
    cds = ColumnDataSource(df)
    source.data = dict(cds.data)
    print(dummysel.value)
    dummysel.value = 'y' # change to value to trigger the download
    print(dummysel.value) # correctly shows changed value (from n to y) but download doesn't happen
    print(source.data)

dlbutton = Button(label="print", button_type="success",width=100)


js_code = """

function table_to_csv(source) {
    const columns = Object.keys(source.data)
    const nrows = source.get_length()
    const lines = [columns.join(',')]
    

    for (let i = 0; i < nrows; i++) {
        let row = [];
        for (let j = 0; j < columns.length; j++) {
            const column = columns[j];
            row.push(source.data[column][i].toString());
        }
        lines.push(row.join(','))
    }

    return(lines.join('\\n').concat('\\n'));
}

let lines = table_to_csv(source);

console.log(lines)


    const filename = 'data_result.csv'
const filetext = table_to_csv(source)
const blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' })

//addresses IE
if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, filename)
} else {
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.download = filename
    link.target = '_blank'
    link.style.visibility = 'hidden'
    link.dispatchEvent(new MouseEvent('click'))
}
"""


dummysel.js_on_change('value',CustomJS(args=dict(source=source),
                                   code=js_code))

dlbutton.on_click(upd)

col = column(dlbutton, dummysel)
curdoc().add_root(col)
2 Likes

@nghenzi, thanks a bunch for your interest to help. That’s awesome! After some trial and error i figured out what I was doing wrong: I had taken out the dummy widget out of curdoc (I had thought it just wont display and the rest of the func stays). When i add the widget to curdoc it now works fine (with & without passing ‘e’ to the upd function as well as with ‘\n’ & with “\\n” in JS too). I have set the .visible attribute to False (but widget added to curdoc) and it is now working as intended.
Thanks a bunch for your help!

Sorry to note that you had issues in making my as-is code to work. I had copy pasted the code (fully working on my side) and selected ‘Preformatted text’. When I copy the code from my post here I see the code with proper format in pycharm. Please let me know what differently i should have done while pasting the code.

@Bryan for his ref too.