Triggering CustomJS when length of CDS changes

I’ve got a complicated setup I don’t really need to get into, but essentially I have a ColumnDataSource (CDS) that the user can append to. It’s set up such that their append operations will only add one “row” at a time, with the necessary checks/safeguards ensuring all CDS fields get a value for the appended row (i.e. maintaining consistent lengths).

I’m wondering if there’s a way to detect that “append” operation being executed (i.e. a change in the length of one of the fields in the CDS).

I know I can do src.js_on_change(‘data’,CustomJS(…)), but the user also has the means of editing existing items in the CDS, and I don’t want to trigger a callback when that happens.

I also suppose I could ID all the ways the user is able to execute that append operation (i.e. add a new callback to when they click the append button etc.). But it’d be a lot cleaner if I could simply trigger a callback on changes to CDS data length. Any thoughts? Thanks…

I think most direct approach would be just checking CDS dims on the data change and redirecting appropriately on change in length.

I don’t think that will work as I don’t know what the length is “before” the user decides to append something.

I did some investigation. Check this out:

from bokeh.models import ColumnDataSource, TapTool, CustomJS
from bokeh.plotting import figure, show


src = ColumnDataSource(data={'x':[0,1],'y':[0,1]})

f = figure()
r = f.scatter(x='x',y='y',source=src,size=8)

#basically, click a point, and a new one will get added
tt = TapTool(renderers=[r]
             ,callback=CustomJS(args=dict(src=src)
                                ,code='''
                                var new_data = {'x':[src.data['x'].at(-1)+1]
                                            ,'y':[src.data['y'].at(-1)+1]}
                                src.stream(new_data)
                                // src actually has a length property on the JS side...
                                // watch this increase in number as you add more points
                                console.log(src.length) 
                                '''))
#but if i try to trigger js_on_change on 'length'... 
#this causes an initialization error                             
# src.js_on_change('length',CustomJS(args=dict(src=src),code='''console.log(src.length)'''))
f.add_tools(tt)
show(f)

The console error I get if i uncomment out the src.js_on_change line:

There’s definitely a difference between the “length” property and say the “data” property on the JS side, so users can add js_on_change “listeners” to be added for changes to the data property, but not for the length property? The length property also doesn’t exist on the python side. I don’t really understand how/why this is set up like this, just sharing my findings messing around with it :sweat_smile:

The JS length is just a a plain basic JS getter, it’s not a Bokeh model property. And the reason for that is that if length was a separate, settable Bokeh property, it could be possible to set it to a value that is inconsistent with the actual length of .data, which that is definitely undesirable. And for many years a settable property is all that would have available. However, these days readonly Bokeh properties are a possibility. I think it would be reasonable to add a read-only length property to CDS, and then you could use all the standard on_change, js_on_change machinery with it. Please open a GitHub Issue if you’d like to request it as a new feature.

Thanks for weighing in. I’ll put in a feature request/move the discussion there.

For the record I ended up just adding an additional callback to all the widgets that add rows to the CDS of interest and it works fine. Just not as clean as it could be, especially if I keep getting more and more feature requests for different ways of appending new data.