Regenerate Plot with Categorical Axis on Widget Change with Flask

I have an application that creates a source out of a tsv file and uses that source to construct a dotplot. In the same application is a python method that packages widgets and customJS controls for the plot into a returned variable. The widgets are working fine.

However, my plot uses a categorical x-axis (called taxa) with a lot of unique values - I used the xFactorRange method to get those out of the tsv. It’s nice to be able to filter my data, but it would be way better if, on filtering my data, the whole dotplot was redrawn - so as to exclude the categories whose other criteria didn’t pass filtering muster from appearing on the x-axis. I already know the BokehDataSource governs the construction of the dotplot - no new source, no new plot. So really what I’m asking is - can I get my JS callback to filter an existing source to make a new source, and kick that source back to dotplot construction?

I’ve seen a few other threads trying to tackle this problem over the years, though there is one additional complication - the JS and plots are passed to html through Flask (a web-framework library). To get the JS to play nicely, I package the controls and logic into their own variables, which are passed to Flask. There are a lot of widgets (so a lot of controls and a lot of logic to go with them).

This code below won’t be enough for you to launch a Flask application - there is no way I can do that in the context of a Bokeh forum post. Instead, it demos how the dotplot, JS, and webpage are constructed and how variables are being passed around. If there is something that can be done within the Javascript - or maybe, within the loop of the callback for each of the widgets in the controls - to completely redraw the plot with a fresh BokehDataSource instead of just filtering the existing BokehDataSource, please let me know.

Dotplot Stuff:

x_range = FactorRange(*["taxa"])
dotplot = figure(x_range=x_range, y_range=(0, max(['raw_rd_cnt'])))'taxa', 0.0, range=dotplot.x_range), y="active_axis", source=source)
return dotplot

Javascript Controls and Logic:

for i in range(len(source)):
            source = source[i]
            controls = {
                "pathogen": MultiSelect(title="Pathogenic", value=["Yes", "No"], options=["Yes", "No"]),
                "raw_rd_cnt": Slider(title="Minimum Raw Read Count", value=0, start=0, 
                                                end=max(['raw_rd_cnt']), step=1)

            controls_array = controls.values()

            callback = CustomJS(args=dict(source=source, controls=controls), code="""

                if (!window.full_data_save) {
                        window.full_data_save = JSON.parse(JSON.stringify(;
                var full_data = window.full_data_save;
                var full_data_length = full_data.taxa.length;
                var new_data = { taxa: [], pathogen:[], raw_rd_cnt:[]}

                for (var i = 0; i < full_data_length; i++) 

                    if (
                        full_data.pathogen[i] == controls.pathogen.value &&
                        full_data.raw_rd_cnt[i] > controls.raw_rd_cnt.value
                      ) { 
                         Object.keys(new_data).forEach(key => new_data[key].push(full_data[key][i]));
      = new_data;


return controls, controls_array, callback

Flask Stuff:

for control in controls_array:
    single_control.js_on_change('value', callback)

inputs_column = column(*controls_array, width=320, height=1000)
layout_row = row(inputs_column, dotplot)

script, div = components(layout_row)
# This statement can be confusing: components() RETURNS a <script> containing the data for the plots passed as arguments to the method.
        # It also returns a <div> that is the target for where the plot view is displayed.

return render_template('demo-webpage.html',
                                      plot_script=script, plot_div=div,

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.