How do I use a TextInput from the user to download in a CSV file the top N rows sorted as per the users sort from a Bokeh DataTable?

I would like to use the TextInput from the user to download the number of specified rows in a CSV file on clicking the download Button. I have used the code from here: How to Download a DataTable as a .csv in a bokeh_server app

In the code below the value of the argument “dwnld_n” doesn’t get updated as per the updated value of the “dwnld_top.value” instead it remains as the initial value of “dwnld_top.value” which in this case is 3.

from bokeh.models.callbacks import CustomJS
import pandas as pd
from bokeh.plotting import ColumnDataSource
from bokeh.models.widgets import Button, DataTable, TableColumn
from bokeh.models import TextInput
from bokeh.layouts import row, column
from bokeh.io import curdoc

js_dwnld = """
var top_n = dwnld_n;
var csv = src.data;
var filetext = 'a,b,c\\n';
for (i=0; i<top_n; i++) {    
    var currRow = [csv['a'][i].toString(),
                   csv['b'][i].toString(),
                   csv['c'][i].toString().concat('\\n')]
    var joined = currRow.join();
    filetext = filetext.concat(joined);    
}
var filename = 'results.csv';
var blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' });
if (navigator.msSaveBlob) { // IE 10+
    navigator.msSaveBlob(blob, filename);
} 
else {
    var link = document.createElement("a");
    if (link.download !== undefined) { // feature detection
        // Browsers that support HTML5 download attribute
        var url = URL.createObjectURL(blob);
        link.setAttribute("href", url);
        link.setAttribute("download", filename);
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }
}
"""

data = pd.DataFrame({'a':[1,2,3], 'b':[2,3,4], 'c':['d','e','f']})
tab_data = ColumnDataSource(data)
tab_cols = [TableColumn(field="a", title="a"), TableColumn(field="b", title="b"), TableColumn(field="c", title="c")]
tab = DataTable(source=tab_data, columns=tab_cols)

dwnld_top = TextInput(value=str(len(data)), title="Number of rows to download")
dwnld = Button(label='Download')
dwnld.js_on_click(CustomJS(args=dict(src=tab_data, dwnld_n=int(dwnld_top.value)), code=js_dwnld))

layout = row(column(dwnld_top, dwnld), tab)

curdoc().add_root(layout)

It would be great if there is a solution to download the data ordered as per the user’s sort applied on the DataTable.

Link to question on stackoverflow: https://stackoverflow.com/questions/62653275/how-do-i-use-a-textinput-from-the-user-to-download-in-a-csv-file-the-top-n-rows

I would greatly appreciate any help on this. Thanks!

@PSK Your problem is here:

dwnld_n=int(dwnld_top.value))

The args parameter is for mirroring Bokeh objects automagically between Python and JavaScript. You should pass the actual TextInput object instead:

dwnld_top=dwnld_top

Then in the JS code you can access dwnld_top.value

That’s great, thanks a lot @Bryan. I’ll post a link to this page as the answer on stackoverflow.

@Bryan is there a way to preserve the sorting of the DataTable when I download it?

@PSK The order of the CSV file is dertermined entirely by the loop in the JS code that appends the lines to the output. It currently just dumps out the data in the order it happens to be in the CDS. You could sort that, but you’d have to do it yourself. i.e. you can sort a list of indices [1…N] according to one of the CDS columns, then use the sorted indices to traverse the CDS in the sorted order when you are dumping the rows.

A “sorted traverse” seems like a reasonable thing to add to BokehJS (and a nice issue for a new contributor). Please feel free to open a GitHub issue to discuss it.

@Bryan From what I understand, this is me sorting the CDS as I would like. But what I would really like is for the CDS to be sorted according to the sort (order) of the DataTable. So for example, if I sorted the DataTable by the column ‘a’ by clicking on the column name (header) in the DataTable and then clicked download I would want that order to be retained in the CSV file. I think what I want is similar to this: Attribute that captures the ordering of the data in a bokeh DataTable

Once I understand that we are on the same page, if you still think it is reasonable to open an issue, I would be more than happy to do that.

Thanks for being so responsive!

Unfortunately I don’t know how to obtain the current UI-selected sort column from the DataTable. The underlying widget is implemented with the third-party SlickGrid library and I am not sure it exposes that information directly. If you can find information about how to determine the current sort column on SlickGrid I can see how it might be applied (e.g. in a CustomJS callback).

Is this helpful?

I’m also not sure if it works when you sort by multiple columns.

Hi @Bryan, could you please give me any suggestions on how I could take this forward?

Im afraid there is not really any great way. To get ahold of the actual SlickGrid instance you have to first get ahold of the Bokeh DataTableView and that is only available by digging through the global Bokeh.index. And if the table is inside any layout, you would need to go through private API as well. As an example, to get to it in the data_tables.py example in the repo, it required:

Array(...Bokeh.index[1056]._child_views.values())[1].grid.getSortColumns()

You will need either to use detailed specific information about the layout that will break if the layout changes at all, or writing some custom generic function that can traverse the graph looking for the data table view that you want. But, if you are willing to do all that, then you could in principle use the result of getSortColumns to do the sorting of the CDS.