0

I have a static Bokeh web app for local use and I want to be able to load a file using javascript without running python. The idea is to be able to share Bokeh output_html file with other non-python users to run it and load their data with a file selector for interactive visualization. I made a very rough code based on this post and this post

I have no knowledge in JS and I apologize in advance for the bad implementation. Please feel free if you have a similar example or a simpler approach to read a file without bokeh server.

from bokeh.models.widgets import Toggle
from bokeh.plotting import figure, output_file, show

output_file("load_data_buttons.html")

x = [0]
y = x

source = ColumnDataSource(data=dict(x=x, y=y))

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

callback = CustomJS(args=dict(source=source), code="""
       // initialize our parsed_csv to be used wherever we want
var parsed_csv;
var start_time, end_time;

        // document.ready
        $(function() {

          $('.load-file').on('click', function(e) {
            start_time = performance.now();
            $('#report').text('Processing...');

            console.log('initialize worker');

            var worker = new Worker('worker.js');
            worker.addEventListener('message', function(ev) {
              console.log('received raw CSV, now parsing...');

              // Parse our CSV raw text
              Papa.parse(ev.data, {
                header: true,
                dynamicTyping: true,
                complete: function (results) {
                    // Save result in a globally accessible var
                  parsed_csv = results;
                  console.log('parsed CSV!');
                  console.log(parsed_csv);

                  $('#report').text(parsed_csv.data.length + ' rows processed');
                  end_time = performance.now();
                  console.log('Took ' + (end_time - start_time) + " milliseconds to load and process the CSV file.")
                }
              });

              // Terminate our worker
              worker.terminate();
            }, false);

            // Submit our file to load
            var file_to_load = document.getElementById("myFile").files[0];

            console.log('call our worker');
            worker.postMessage({file: file_to_load});
          });

        });

        x =  parsed_csv.data['x']
        y =  parsed_csv.data['y']
        #load data stored in the file name and assign to x and y
        source.trigger('change');
    """)

toggle1 = Toggle(label="Load data file 1", callback=callback)

layout = Row(toggle1, plot)

show(layout)

worker.js

self.addEventListener('message', function(e) {
    console.log('worker is running');

    var file = e.data.file;
    var reader = new FileReader();

    reader.onload = function (fileLoadedEvent) {
        console.log('file loaded, posting back from worker');

        var textFromFileLoaded = fileLoadedEvent.target.result;

        // Post our text file back from the worker
        self.postMessage(textFromFileLoaded);
    };

    // Actually load the text file
    reader.readAsText(file, "UTF-8");
}, false);

The csv file has x,y data

x   y
0   0
1   1
2   2
3   3
4   4
ys4503
  • 27
  • 5

1 Answers1

1

You don't need Web Workers to achieve it, especially if you're not comfortable with JavaScript. Here's how I would do it:

from bokeh.layouts import row, column
from bokeh.models import Div, ColumnDataSource, CustomJS, FileInput
from bokeh.plotting import figure, save

source = ColumnDataSource(data=dict(x=[0], y=[0]))

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

fi_label = Div(text='Load data file 1')
fi = FileInput()
status = Div()

callback = CustomJS(args=dict(source=source,
                              status=status),
                    code="""
status.text = 'Loading...';
Papa.parse(atob(cb_obj.value), {
    // For some reason, Papa didn't detect it automatically.
    delimiter: '\t',
    header: true,
    dynamicTyping: true,
    complete: function (results) {
        const acc = results.meta.fields.reduce((acc, f) => {
            acc[f] = [];
            return acc;
        }, {});
        source.data = results.data.reduce((acc, row) => {
            for (const k in acc) {
                acc[k].push(row[k]);
            }
            return acc;
        }, acc);
        status.text = 'Ready!';
    }
});
""")

fi.js_on_change('value', callback)

template = """\
{% block preamble %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.1.0/papaparse.min.js"
        integrity="sha256-Fh801SO9gqegfUdkDxyzXzIUPWzO/Vatqj8uN+5xcL4="
        crossorigin="anonymous"></script>
{% endblock %}
"""
# Cannot use `show` because it doesn't have the `template` argument.
save(column(row(fi_label, fi), plot), template=template)
Eugene Pakhomov
  • 9,309
  • 3
  • 27
  • 53
  • This is excellent. I added ```source.change.emit();``` at the end of the js code to update the figure, and ```worker: true,``` before delimiter. I noticed without worker parameter and file with >50pts, the figure does not update until I interact with it, while with it, the figure is updated very quick. Another thing to mention is Bokeh FileInput doesn't trigger the callback if you load the same file, even if you change the name. I think it's because the fi 'value' doesn't change. I solved that by adding a button to reload the file. I learned a lot with your help. Thanks and much appreciated. – ys4503 Apr 23 '20 at 00:56
  • `source.change.emit();` should not be needed as I set the full data with `source.data = ...` - it will trigger the `change` event by itself. – Eugene Pakhomov Apr 23 '20 at 05:04
  • For some reason, the figure doesn't update most of the time until I interact with it if I don't use the emit line. I just looked it up and found this [link](https://github.com/bokeh/bokeh/issues/7106) and [here](https://github.com/bokeh/bokeh/pull/6483). From the second link: "emit is necessary to trigger internal bokehjs' signals, because this requires ```!isEqual(old_data, new_data)```". I'm out of my depth here so please excuse my ignorance. – ys4503 Apr 23 '20 at 19:59
  • Those are pretty old links. Are you sure you're using the latest Bokeh? In any case, if that works for you with the extra call to `emit()`, then all is good - it's not a big deal. – Eugene Pakhomov Apr 24 '20 at 06:08
  • Hi, a follow-up question: if the file contains dynamic number of series (x,y) like in one file only x,y, and another x,y,x,y. Would it be possible to plot them all (preferably with drop-down list)? I'm only looking for a simple answer with a hint if possible before I try at it. Right now, it will always read the last x,y in file and plot them. The JS will have no issues to map the data but not sure if ColumnDataSource can accept dynamic data. Maybe two CDSs one with indx and one with data. I will try it if you think it is doable and maybe start another post if I get stuck. – ys4503 Apr 27 '20 at 03:42
  • It's possible. You can rename the fields from `x,y,x,y` (which is a bad way of naming columns anyway) to `x1,y1,x2,y2` when you load the data - CDS won't have any problems with that, and then your drop-down could just switch the columns in the glyph renderers. – Eugene Pakhomov Apr 27 '20 at 05:22