Understanding change.emit() with filters/sources/glyphs

Hi,

I am having quite the time understanding change.emit(). See my example below to see the struggle. My example has one datasource. Two buttons do two different callbacks: one will change the ys column of the ColumnDataSource being used, and the other will alter a GroupFilter being applied via CDSView (to toggle between colours). The callback for the column change made sense to me - renderer.glyph.change.emit() was intuitive. However the view/filter took a lot of trial and error - and I think I fundamentally don’t understand a few things which is leading to my struggle. There is a whole lot of “I can make it work but don’t know why it works” going on.

So what I don’t understand:

ITEM 1 - source.change.emit() will update glyphs even if the source itself doesn’t change. In both ycb and ccb in my example, I found that just adding src.change.emit() would result in things “working”, even though (in my head) I’m not actually changing the source - I’m either changing the column the glyph is reading for its values (ycb) or the group name the filter is using (ccb)… in both cases the source doesn’t change… why does this work then?

ITEM 2 - finding the specific thing to put .change.emit() on for the filter was way harder for me than I thought it should be. See all my comments in the ccb callback… I tried a bunch of things… can someone explain why what I arrived at worked while the other attempts don’t?

from bokeh.models import MultiLine, ColumnDataSource, Button, CustomJS, CDSView, GroupFilter
from bokeh.plotting import figure,show
from bokeh.layouts import layout

data = {'xs':[[0,2,2,0],[3,4,4,3],[5,6,6,5],[8,10,10,8]]
        ,'ys1':[[0,0,2,2],[3,3,4,4],[5,5,6,6],[8,8,10,10]]
        ,'ys2':[[0,0,3,3],[6,6,3,3],[2,2,4,4],[7,7,11,11]]
        ,'c':['blue','green','blue','green']}

src = ColumnDataSource(data)

#fig
fig = figure()
glyph = MultiLine(xs='xs',ys='ys1',line_color='c')
view = CDSView(source=src,filters=[GroupFilter(column_name='c',group='blue')])
r = fig.add_glyph(src,glyph,view=view)

#button to change the y source - this works nicely and "makes sense" to me
ybtn = Button(label = 'ysource1')

ybcb = CustomJS(args=dict(r=r,ybtn=ybtn)
               ,code='''
               if (ybtn.label == 'ysource1'){
                   ybtn.label = 'ysource2';
                   r.glyph.ys.field = 'ys2';
                   r.glyph.change.emit()
                   //THIS ALSO WORKS... BUT WHY:
                   //src.change.emit()
                   }
               else if (ybtn.label == 'ysource2'){
                   ybtn.label = 'ysource1';
                   r.glyph.ys.field = 'ys1';
                   r.glyph.change.emit()}
              
               ''')
ybtn.js_on_click(ybcb)

#button to apply a cdsview filter for color.. the callback behaviour... is confusing to me.
cbtn = Button(label = 'blue')

ccb = CustomJS(args=dict(r=r,cbtn=cbtn,view=view,src=src)
               ,code='''
               if (cbtn.label == 'blue'){
                   cbtn.label = 'green';
                   view.filters[0].group = 'green';
                   //SRC.CHANGE.EMIT() MAKES THIS WORK.. 1) WHY DOES IT WORK IF THE SOURCE ITSELF ISN'T CHANGING?  
                   //src.change.emit()
                   //BUT HOW DO I EMIT ONLY THE FILTER UPDATE CHANGE?

                   // THINGS I TRIED
                   // view.properties.change.emit() // view.properties returns undefined
                   // view.change.emit() // get nothing
                   //view.filters[0].change.emit() // get nothing
                   //r.properties.view.filters.change.emit() // returns undefined
                   //r.change.emit() // does nothing
                   //r.glyph.change.emit() // does nothing
                   //IT LOOKS LIKE I HAVE TO EMIT THE CHANGE ON THE RENDERER'S VIEW'S FILTER...
                   // THIS WORKS AFTER MUCH TRIAL AND ERROR AND CONSOLE.LOG-ING EVERYTHING etc.
                   r.view.properties.filters.change.emit()
                   }
                   
               else if (cbtn.label == 'green'){
                   cbtn.label = 'blue';
                   view.filters[0].group = 'blue';
                   //SRC.CHANGE.EMIT MAKES THIS WORK
                   //src.change.emit();
                   // SAME THING THAT WORKS AS ABOVE...
                   r.view.properties.filters.change.emit()
                   }
                   
               ''')
cbtn.js_on_click(ccb)

lo = layout([[fig,ybtn,cbtn]])
show(lo)

As always thank you so much for this library and ongoing support!

@gmerritt123 source.change.emit() causes a redraw because somewhere in BokehJS there is a line like:

this.connect(this.model.data_source.change, update)

that means “do an update whenever the signal is emitted”.

There are things Bokeh can detect automatically and emit the signal for, e.g. if you assign to a new value to source.data, then BokehJS will emit the change signal automatically. But ultimately, it does not matter what reason the signal is emitted for, just that it is emitted at all. I.e. if you manually call source.change.emit(), then you are stipulating that the update should happen.

As for the other question, if a signal emit has no effect, that just means that there is not any this.connect on the other end to set up a handler for the signal. Most of the time that’s because there shouldn’t be any handler set up, but sometimes there is just “missing plumbing”

In this particular case, you could do view.filters.change.emit(). It’s not ever intended that users use or go through .properties and it should never be necessary. Alternatively, personally I would probably just call source.change.emit() since it is simpler and I know it will have the desired effect (a redraw).

Final comment: in general, Bokeh can automatically detect actual assignments but cannot automatically detect “in place changes”

source.data = new_data // Bokeh can detect, no manual emit needed

source.data["foo"][10] = 10 // Bokeh cannot detect, manual emit needed

So if you can structure your updates in terms of real assignments to properties, then you can often avoid needing to call emit at all.

3 Likes

Thanks so much for elaborating, and for providing advice on rules of thumb to try to adhere to :smiley:

1 Like

Hi, sorry to dredge this thread up, but at least on Bokeh 2.3.2 and Python 3.9.4 that @Bryan’s suggestion to do view.filters.change.emit() doesn’t seem to work for me — the view.filters object does not have a change method attached to it, at least if I’m parsing what the debugger is telling me correctly. Here’s a streamlined code example based on the original snippet that I’m using to test:

from bokeh.models import MultiLine, ColumnDataSource, Button, CustomJS, CDSView, GroupFilter
from bokeh.plotting import figure,show
from bokeh.layouts import layout

data = {'xs':[[0,2,2,0],[3,4,4,3],[5,6,6,5],[8,10,10,8]]
        ,'ys1':[[0,0,2,2],[3,3,4,4],[5,5,6,6],[8,8,10,10]]
        ,'c':['blue','green','blue','green']}

src = ColumnDataSource(data)

#fig
fig = figure()
glyph = MultiLine(xs='xs',ys='ys1',line_color='c')
view = CDSView(source=src,filters=[GroupFilter(column_name='c',group='blue')])
r = fig.add_glyph(src,glyph,view=view)

#button to apply a cdsview filter for color.. the callback behaviour... is confusing to me.
cbtn = Button(label = 'blue')

ccb = CustomJS(args=dict(cbtn=cbtn,view=view)
               ,code='''
               if (cbtn.label == 'blue'){
                   cbtn.label = 'green';
                   view.filters[0].group = 'green';
                   view.filters.change.emit(); // Does not work!
                   //view.properties.filters.change.emit(); // This does still work, however
                   }
                   
               else if (cbtn.label == 'green'){
                   cbtn.label = 'blue';
                   view.filters[0].group = 'blue';
                   view.filters.change.emit(); // Does not work!
                   //view.properties.filters.change.emit() // This does still work, however
                   }
                   
               ''')
cbtn.js_on_click(ccb)

lo = layout([[fig],[cbtn]])
show(lo)

The traceback is:

VM2042:8 Uncaught TypeError: Cannot read property 'emit' of undefined
    at c.eval (eval at get func (bokeh-2.2.3.min.js:381), <anonymous>:8:40)
    at i.execute (bokeh-2.2.3.min.js:381)
    at u._process_event (bokeh-2.2.3.min.js:250)
    at y.trigger (bokeh-2.2.3.min.js:166)
    at u.trigger_event (bokeh-2.2.3.min.js:250)
    at s.click (bokeh-widgets-2.2.3.min.js:55)
    at HTMLButtonElement.<anonymous> (bokeh-widgets-2.2.3.min.js:46)

I looked through the patch notes for recent releases, but I didn’t see anything obvious where using view.filters.change.emit() was deprecated or removed. Am I doing something wrong?

In case it matters, I’ll note that I’m testing in a Jupyter notebook.