CustomJS not updating plot

# Get map tiles
tile_provider = get_provider(CARTODBPOSITRON)

# Create ColumnDataSource from coordinates, sizes and predicted means
source = ColumnDataSource(dict(x=x_filtered, y=y_filtered, sizes=sizes_filtered, means=mean_filtered, 
                                vars=np.sqrt(vars_filtered), alphas=alphas, lat=lat_filtered, lon=lon_filtered))

# Create linear colour map for pollution data
mapper = linear_cmap(field_name='means', palette=YlOrBr[9][::-1] ,low=0 ,high=max(mean_filtered)[0])

# Tooltips
tooltips = [("Lat/long", "@lat, @lon"),
            ("Predicted PM2.5", "@means"),
            ("Predicted Variance", "@vars")]

# Plot figure and add tiles 
scaled_map = figure(title=None, x_range=(min(x), max(x)), y_range=(min(y), max(y)),
           x_axis_type="mercator", y_axis_type="mercator", tooltips=tooltips, tools = "pan,wheel_zoom,box_zoom,reset,hover")

scaled_map.add_tile(tile_provider)


# Create and plot glyphs for predictions
glyph = Circle(x="x", y="y", radius="sizes", line_color="white", fill_color=mapper, fill_alpha=0.8, line_width=1, 
                line_alpha=1)
scaled_map.add_glyph(source, glyph)

# Add colour bar 
color_bar = ColorBar(color_mapper=mapper['transform'], width=8,  location=(0,0))
scaled_map.add_layout(color_bar, 'right')

callback = CustomJS(args=dict(source=source, xr=scaled_map.x_range, yr=scaled_map.y_range), code="""
    
    var xstart = xr.start;
    var xend = xr.end;
    var ystart = yr.start;
    var yend = yr.end;
    var gridsize = 20;
    var url = "http://127.0.0.1:5000/?xstart="+xstart+"&xend="+xend+"&ystart="+ystart+"&yend="+yend+"&gridsize="+gridsize;
    var data = source.data;
    var x = data["x"];
    var y = data["y"];
    var means = data["means"];
    
    $.getJSON(url, function(request){
        x = request.xgrid;
        y = request.ygrid;
        means = request.means;});
            
    source.change.emit();    
    """)

scaled_map.js_on_event(LODEnd, callback)

output_notebook()
show(scaled_map)
# Save the plot by passing the plot -object and output path
#save(obj=scaled_map, filename=outfp)

Hi, I’m trying to get a CustomJS script to update a grid from an API when I zoom in. I’m using getJSON to call the API endpoint using the current x and y ranges and returning an array. I think I have the callback set up correctly (but possibly not, this is my first time using javascript) and it’s pinging the server but I’m not getting new points. Can anyone help? I feel like I’ve missed something simple but struggling to spot it!

Many Thanks

Hi @Dennis_M_Reddyhoff You have to actually modify source.data by assigning to it (or assigning into it, e.g. source.data['x'] = ... ) The code above creates new local variables x, y, and means that have no bearing on the value of source.data, and then immediately throws them away.

Hi Bryan, I’ve changed the code a little to create the ColumnDataSource from the API too for consistency, I changed what you said but still no luck

init_json = requests.get(url).json()

# Get map tiles
tile_provider = get_provider(CARTODBPOSITRON)

# Create ColumnDataSource from coordinates, sizes and predicted means
source = ColumnDataSource(dict(x=init_json["xgrid"], 
                               y=init_json["ygrid"], 
                               sizes=init_json["sizes"], 
                               means=init_json["means"], 
                               vars=np.sqrt(init_json["vars"]).tolist(), 
                               lat=init_json["lat"], 
                               lon=init_json["lon"]))

# Create linear colour map for pollution data
mapper = linear_cmap(field_name='means', palette=YlOrBr[9][::-1] ,low=0 ,high=max(init_json["means"]))

# Tooltips
tooltips = [("Lat/long", "@lat, @lon"),
            ("Predicted PM2.5", "@means"),
            ("Predicted Variance", "@vars")]

# Plot figure and add tiles 
scaled_map = figure(title=None, x_range=(min(x), max(x)), y_range=(min(y), max(y)),
           x_axis_type="mercator", y_axis_type="mercator", tooltips=tooltips, tools = "pan,wheel_zoom,box_zoom,reset,hover")

scaled_map.add_tile(tile_provider)


# Create and plot glyphs for predictions
glyph = Circle(x="x", y="y", radius= "sizes", line_color="white", fill_color=mapper, fill_alpha=0.8, line_width=1, 
                line_alpha=1)
scaled_map.add_glyph(source, glyph)

# Add colour bar 
color_bar = ColorBar(color_mapper=mapper['transform'], width=8,  location=(0,0))
scaled_map.add_layout(color_bar, 'right')

callback = CustomJS(args=dict(source=source, xr=scaled_map.x_range, yr=scaled_map.y_range), code="""
    
    var xstart = xr.start;
    var xend = xr.end;
    var ystart = yr.start;
    var yend = yr.end;
    var gridsize = 20;
    var url = "http://127.0.0.1:5000/?xstart="+xstart+"&xend="+xend+"&ystart="+ystart+"&yend="+yend+"&gridsize="+gridsize;
    
    $.getJSON(url, function(request){
        source.data['x'] = request.xgrid;
        source.data['y'] = request.ygrid;
        source.data['means'] = request.means;
        source.data['vars'] = request.vars;});
        source.data['sizes'] = request.sizes;
        source.data['lat'] = request.lat;
        source.data['lon'] = request.lon;});
        
    source.change.emit();    
    """)

scaled_map.js_on_event(LODEnd, callback)

output_notebook()
show(scaled_map)

@Dennis_M_Reddyhoff I am afraid I can’t say much more without complete code to run. At this stage pesonally I would be adding console.log and/or debugger statements to the getJSON code to make sure it is executing and that the state I expect to be present is present.

Thanks, I’ll try and think of a toy example. The full code requires you to start a Flask server to serve as the API so not sure that it will be easy to work through on here. I’ll look in to the debugging options you posted as well

As a last thing, am I missing something in updating the glyph? When I call js_on_event through the map and update the source, will the glyphs be automatically updated from that source as well?

Debugging in my browser, the error is Uncaught ReferenceError: $ is not defined. Browsing on the web it looks like I need to include

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

Currently I’ve just been pushing the bokeh map to an html file, do I need to do something different to include this line when the plots are output so I can use jquery in CustomJS?

Oh, Bokeh does not use or provide jquery. Normally you would need to embed Bokeh content in to your own HTML template that loads jquery. I suppose you could do that in the notebook, but there is probably some way to load jquery using the %javascript cell magic (I think that’s it).

Alternatively you can use plain native JS code instead of jquery.

Ahh ok! I’ve jumped a step, thanks for getting me on the right track anyway, I’ll look in to your suggestions

I ended up using

fetch(url)        
        .then(
            function(response) {
              response.json().then(function(data) {
                source.data['x'] = data.xgrid;
                source.data['y'] = data.ygrid;
                source.data['means'] = data.means;
                source.data['vars'] = data.vars
                source.data['sizes'] = data.sizes;
                source.data['lat'] = data.lat;
                source.data['lon'] = data.lon;
                });
            }
      )

instead of jquery and that has kind of worked, it’s at least updating stuff although still a bit buggy.

@Dennis_M_Reddyhoff There is a hard assumption that all CDS columns are always all the same length at all times (e.g. just like a DataFrame). So if your new data can have a different lenght, then updating it one column at a time will break that assumption and I would expect weird behavior. It’s almost always a better pattern to update things “all at once”

const new_data = { ... }
source.data = new_data

Another nice benefit of this approach is that the explicit source.change.emit() is not necessary (full assignments to .data can be detected and handled automatically).

You answered my next question! I’ve renamed a few bits and now can just use

fetch(url)        
        .then(
            function(response) {
              response.json().then(function(data) {
                source.data = data;
                });
            }
      )

Without using emit, it’s now super responsive and working exactly how I pictured it! Thanks for all your help

1 Like