How to Trigger CustomJS on Data Change?

I am building an interactive datatable that colors the cell background based on a field in the underlying CDS. Multiple separate controls on the page can change what rows are being displayed.

I am trying to implement an automatic banding system that alters the brightness of the color based on the color value and a rotating integer of how many of that color have been encountered while walking the currently visible set of rows.

My current thought is creating some CustomJS code and attaching it to the datasource using js_on_change.
However, I am not finding a list of events supported by CDS, and am a little wary of getting into an infinite loop if a

source.change.emit()

at the end of the banding code re-triggers execution.

Any suggestions as to what event to use with CDS js_on_change? Is there some documentation on events that involve data sources and not widgets?

1 Like

.change.emit is a something you can call in general on any Bokeh model or property to say “this changed”, and if there are any event listeners (e.g. to request a redraw or whatever) they will be notified. However, it’s not generally necessary to call this yourself. If you are updating a Bokeh property by actual assignment, e.g.

source.data = new_data  

Then BokehJS can automatically detect this and will trigger its own change event for things. Really, the only time you need to call .change.emit yourself is if you are mutating some composite data object in place. The most common example is something like:

source.data[3] = 5.7   

BokehJS can’t automatically detect changes “inside the array”, so that is a case where you would need to add source.change.emit() after your changes.

1 Like

@Bryan, sorry if I was unclear. My core need is to know what events I can use for the Event parameter in the ‘‘js_on_change’’ or “js_on_event” call to register my javascript to be executed when the IndexFilter or ColumnDataSource or View or (??) are changed.

full_filter.js_on_change(‘Event’, color_banding_callback)

@Kyle_Marcroft js_on_change is completely generic, it can be called on any Bokeh model with (the name of) any property. The events for js_on_event are all listed here:

bokeh.events — Bokeh 2.4.2 Documentation

I guess the string alias names did not get rendered there for some reason. If you’d rather use those, you can look for the event_name class attribute in the source definitions:

bokeh.events — Bokeh 2.4.2 Documentation

Below is my code, @Bryan . When I include the js_on_change line at the bottom, none of my plot, controls, etc. render, I just get a blank white page. When I comment that line out, I get my normal plot. The same javascript code inserted into one of my other control’s callbacks successfully sets the [‘Color’] values and I get my individualized 2-tone banding when that control is used.

I am also a little confused as to why the js_on_change method requires an event to be passed as a parameter. If it is simply to be called if anything in the underlying bokeh model is changed, then why specify any particular event?

color_banding_callback = CustomJS(args=dict(source=merged_source), code='''
    var soc_count_dict = {'SD855': 0, 'E9820': 0, 'P90': 0, 'G90T': 0, 'A13': 0,
                  'SD8cx': 0, 'Default': 0};
                  
    var soc_color_dict = {'SD855': '#FFA07A', 'E9820': '#FFB6C1', 'P90': '#D8BFD8', 'G90T': '#98FB98', 'A13': '#F5DEB3',
                  'SD8cx': '#EE82EE', 'Default': '#FF0000', 'SD855~': '#FDB06A', 'E9820~': '#F0A8B2', 'P90~': '#FFC0CB',
                  'G90T~': '#9ACD32', 'A13~': '#DEB887',
                  'SD8cx~': '#DDA0DD', 'Default~': '#00FF00'};
        
    var data = source.data;
    var workload_index = data["Workload_Index"];
    var data_length = data.length;

    for(i = 0; i < data_length; i++) {
        soc_name = data["SOC"][i];
        if(soc_count_dict[soc_name] %2 == 0) {
            data["Color"][i] = soc_color_dict[soc_name];
            soc_count_dict[soc_name] = soc_count_dict[soc_name] + 1
        }
        else {
            data["Color"][i] = soc_color_dict[soc_name.concat("~")];
            soc_count_dict[soc_name] = soc_count_dict[soc_name] + 1
        }   
    }

    source.change.emit();
}
''')

merged_source.js_on_change('tap', color_banding_callback)

You’ve pasted the code as a quotation, can you edit to use code formatting? (triple-backtick fences, or the </> button in the GUI editor)

@Bryan Fixed the quotation/code formatting gaff. Apologies.

Also noticing quickly, this is wrong:

merged_source.js_on_change('tap', color_banding_callback)

“tap” is an event, so you would need to use js_on_event, as I mentioned earlier. js_on_change is only for property changes on Bokeh models.

# respond to a change in slider value
slider.js_on_change('value', ...)

# respond to a tap event
plot.js_on_event('tap', ...)

@Bryan
This code causes my datatable application to come up as a completely white page:

select_to_index_callback = CustomJS(args=dict(source=merged_source), code='''
        //var data = source.data;
        //alert("Hit callback!");
            
    ''')
    merged_source.js_on_change('value', select_to_index_callback)

If I comment out the bottom line, my datatable application comes up and displays fine.
#merged_source.js_on_change('value', select_to_index_callback)

Uncommenting either or both of the lines in the javascript makes no difference. There seems to be a more fundamental problem in play here.

@Kyle_Marcroft ColumnDataSource objects do not have a "value" property so I would expect that to do nothing at best, though failing in some way would not surprise me, either. I was perhaps assuming too much context with the terminology. In Bokeh, properties are very nearly exactly what is listed in the Reference Guide for bokeh.models, e.g. for DataRange1d you can see lots of properties:

Anything listed there could be used with .js_on_change, e.g.

range.js_on_change('end', ...)  # respond when range.end changes 

You can respond to any changes in any property on any Bokeh model in this same way, i.e. anything that is listed as a property in the reference guide is fair game.

Conversely, if something is not listed as a property on a Bokeh model class (or a parent class), then it is not something that will have any relevance with .js_on_change. As I mentioned above, ColumnDataSource does not have any property named "value", so that is definitely not expected to do anything useful.

It’s not really clear to me what you are trying to accomplish so unfortunately I can’t really offer any more specific guidance. I’ll mention that .data is probably the most common CDS property one might care about, but I should reiterate, due to inherent limitations, Bokeh an only detect when .data changes in entirety, i.e. when it is assigned to:

source.js_on_change('data', ...)  # respond when source.data = new_data

There is no good way to pick up changes inside .data. That said, usually callbacks are setting CDS data, not waiting to respond to changes in it. Again it’s unclear what you are tryin to do. Alternatively maybe you want to respond to selections? Selections are stored on a Selection model that itself is a property on a CDS. So that would be something like:

source.selection.js_on_change('indices', ...) # respond when a selection is updated

@Bryan, thank you for your generosity with your time and very verbose response. I will explain what I am trying to do.

I have a datatable that displays rows based on manipulation of the filter associated with the data source. Whenever a pertinent control is activated, I empty the filter by setting its length to 0, then I loop through all the rows of data and append the indices I want shown into the filter.

I also have a button that will save the visible rows and columns to a .csv file.

I want to keep the capability to save all rows shown, and add the capability to save only those rows that are individually Selected.

I added selectable=‘checkbox’ and tested. In the beginning of a test session, the Selected.indices stay synchronized with the index of the row to which they were initially associated. eg. If the 3rd row from the top is index 6, then no matter where that row went by sorting, non-display, display, or what-not, the Selected state would adhere to it and Selected.indices would include 6. But sooner or later while testing the controls that hide/show rows based on various criteria, the Selected indices would un-adhere, and instead stick to the next rows to occupy their screen location. eg. If the 3rd row from the top is selected and is index 6 then the Selected.indices would include 6 and the check would properly display, but then after a rearrangement the third row from the top is another number like 345. This would cause the Selected.indices to no longer contain 6 but instead contain 345, and of course the display would show row 345 “checked”.

I am trying to figure out a way to effectively “lock” the Selected indices to the row indices such that they always adhere. My current tinkering is to try to make a “shadow” set of indices by using an extra IndexFilter. Any direct changes by the user in selecting or unselecting rows get repeated over to the shadow_index, and all other controls that manipulate row visibility or ordering replace the Selected.indices values with the shadow_index values.

@Bryan
I have tried using the selectable=‘checkbox’ and got the behavior from the above reply. I have tried selectable=‘True' and changing the value of a custom checkbox column, but cannot figure out if there is any way to get the display to update from within the CustomJS.

select_to_index_callback = CustomJS(args=dict(full_filter=full_filter, source=merged_source, selected_backup_filter=selected_backup_filter), code='''
            
                var data = source.data;
                var data_length = data['Checked'].length;
                var this_index = cb_obj.indices[0];
                
                alert("Hi: ".concat(cb_obj.indices[0], "   ", data['Checked'][this_index]));
                
                if (data['Checked'][this_index] == '&#x2751') {
                    data['Checked'][this_index] = '&#9989';
                }
                else {
                    data['Checked'][this_index] = '&#x2751';
                }

        ''')
        merged_source.selected.js_on_change('indices', select_to_index_callback)

If I cannot get this working with the Selectable functionality, my next step may have to be trying to get a column of checkboxes to the left of the datatable, one per visible row. For that, I think I need to research how to find out what the top visible row in the datatable is.

@Kyle_Marcroft I think we are somehow talking past one another, maybe some simple but complete examples will help. So lets take things one question at a time:

if there is any way to get the display to update from within the CustomJS.

Yes, there is:

from datetime import date
from random import randint

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import Button, ColumnDataSource, CustomJS, DataTable, DateFormatter, TableColumn

source = ColumnDataSource(data=dict(
    dates=[date(2014, 3, i+1) for i in range(10)],
    downloads=[randint(0, 100) for i in range(10)],
))

columns = [
    TableColumn(field="dates", title="Date", formatter=DateFormatter()),
    TableColumn(field="downloads", title="Downloads"),
]
data_table = DataTable(source=source, columns=columns, width=400, height=280, selectable="checkbox")

button = Button()
button.js_on_click(CustomJS(args=dict(source=source), code="""
    source.selected.indices = [2, 4, 6, 7, 8]
"""))

show(column(button, data_table))

Conversely, you can get the indices that have been set ue to a user interaction:

from datetime import date
from random import randint

from bokeh.io import show
from bokeh.models import ColumnDataSource, CustomJS, DataTable, DateFormatter, TableColumn

source = ColumnDataSource(data=dict(
    dates=[date(2014, 3, i+1) for i in range(10)],
    downloads=[randint(0, 100) for i in range(10)],
))

columns = [
    TableColumn(field="dates", title="Date", formatter=DateFormatter()),
    TableColumn(field="downloads", title="Downloads"),
]
data_table = DataTable(source=source, columns=columns, width=400, height=280, selectable="checkbox")

source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
    console.log(source.selected.indices)
"""))

show(data_table)

You could certainly do something else with those indices, e.g. update corresponding items in CDS columns. Just remember if you update CDS columns “in place” that’s the one time you need a source.change.emit() at the end. There are lots of examples of updating a source.data in the docs and examples in the repo.

Hello @Bryan. Thank you for your patience. Here is some code that demonstrates my basic problem with the selectable checkboxes. I need the checkbox indices to be associated with the logical row indices and not the visual indices.

from datetime import date
from random import randint

from bokeh.io import show
from bokeh.models import ColumnDataSource, CustomJS, DataTable, DateFormatter, TableColumn, Button, CustomJS, IndexFilter, CDSView
from bokeh.layouts import column

my_range = 10

source = ColumnDataSource(data=dict(
    dates=[date(2014, 3, i+1) for i in range(my_range)],
    downloads=[randint(0, 100) for i in range(my_range)],
))

filter = IndexFilter(list(range(0, 10)))
original_filter = IndexFilter(list(range(0, 10)))

view = CDSView(source=source, filters=[filter])

columns = [
    TableColumn(field="dates", title="Date", formatter=DateFormatter()),
    TableColumn(field="downloads", title="Downloads"),
]
data_table = DataTable(source=source, columns=columns, width=400, height=280, selectable="checkbox", view=view)


button_callback = CustomJS(args=dict(view=view, source=source, filter=filter, original_filter=original_filter), code="""
    length = original_filter.indices.length;  
    var data = source.data;
    var indices = filter.indices;
    
    
    if (indices.includes(1)) {
        indices.length=0;
        for (i=1; i < length; i++) {
            if (i%2 == 0) {
                indices.push(i);
            }
            else {
                
            } 
        }
    }
    else {
        indices.length=0;
        for (i=1; i < length; i++) {
            if (i%2 == 1) {
                indices.push(i);
            }
            else {
                
            } 
        }
    }
    source.change.emit();
    
""")

button = Button(label="Button", callback=button_callback)

source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
    console.log(source.selected.indices)
"""))

show(column(button, data_table))

Example

You never mentioned views until this point. In this sitiuation, DataTable needs a little extra help. It does not redraw the visually selected rows unless it thinks the selection has changed. In this case the selection has not changed (all you have one is change the view, the selection is the same, always on the actual full array indices). But you can emit a change event on the selection manually:

button_callback = CustomJS(args=dict(view=view, source=source, filter=filter, original_filter=original_filter), code="""
    length = original_filter.indices.length;
    var indices = []

    if (filter.indices.includes(1)) {
        for (var i=1; i < length; i++) {
            if (i%2 == 0) {
                indices.push(i);
            }
        }
    }
    else {
        for (var i=1; i < length; i++) {
            if (i%2 == 1) {
                indices.push(i);
            }
        }
    }
    filter.indices = indices
    source.change.emit();
    source.selected.change.emit()
""")

button = Button(label="Button")
button.js_on_click(button_callback)

FYI you should use button.js_on_click as the old deprcated adhoc callback arg is about to disappear in the next version.

1 Like

Thank you, @Bryan. Adding source.selected.change.emit() after source.change.emit() to all of my controls’ CustomJS solved my problem for the current project.

Is there some general rule we can take away from this on the scope and timing of emitting changes to objects then attached objects? I had been sort’a thinking that emitting a change from a ‘parent’ object would recursively take care of all of its attached/child objects automagically.

BokehJS signal/listeners are per-object, optionally for individual properties immediately on that object:

# trigger handlers for a specific property (".foo") on a specific object:
#
# this.connect(obj.properties.foo.change, () => alert("obj.foo changed value"))
obj.properties.foo.change.emit()

# trigger handlers for a specific object
#
# this.connect(obj.change, () => alert("any obj.X changed value"))
obj.change.emit()

Setting up connections with .connect is not something I would expect users to ever need to do, except in the case of writing a custom extension, but I’ve shown those corresponding calls for completeness. The question is then what listeners does BokehJS hook up? [1] Sometimes connections are missing or could use adjustment, e.g. perhaps in this case there are changes that would make this use-case work more simply. But IIRC DataTable selection rendering was limited to property changes specifically on Selection objects for performance reasons, so it’s not clear without some investigation. Offhand, adding listeners for views might also be reasonable and not impact table performance.


  1. There is no documentation of this. BokehJS has changed so much and so fast over the years it’s just not possible. Things are only just now getting to a point where it might be feasible and make sense to invest effort into detailing Bokeh’s internal eventing structures. However, it’s also just the case that the common case is what gets priority for limited time and resources, and the vast majority of usage starts and ends with source.data = new_data. ↩︎

1 Like

Cogent and informative as always, @Bryan, thank you.

1 Like

Hey all - sorry to revive an old topic - but is this only a bokeh serve feature? Currently having trouble w/ triggering a custom JS callback when the data source is changed in-browser via another js callback. Is there some kind of trick I’m missing?

My basic use case is: I have a bunch of widgets that all trigger the same JS callback, which filters a data source based on the values of all the widgets. I want to then update the active values of some of the widgets (in this case, a MultiChoice widget) to show only values that exist within the current data source. But despite successfully changing the data source in-browser, I cannot get the callback to trigger after setting it up like so:

data_source.js_on_change(‘data’, choices_callback)

(If this requires a full working example I can try and pull one together, since my current code is quite long, sorry)