javascriptpythonbokeh

I don't understand why the monthly sst is not changing in browser


Hi all, I don't understand why the monthly sintetic sea surface temperature is not changing in the browser after running this script (I checked that monthly data is different):

import numpy as np
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.models import (
    ColumnDataSource, LinearColorMapper, ColorBar, Button, CustomJS, Title
)
from bokeh.layouts import column

Grid:

lon = np.linspace(-180, 180, 18)
lat = np.linspace(-90, 90, 9)
lon2d, lat2d = np.meshgrid(lon, lat)

Generate 12 months of SST data with clear variability:

frames = []
for month in range(12):
    sst = (
        15
        + 10 * np.cos(lat2d * np.pi / 18)
        + 10 * np.sin((lon2d + month * 30) * np.pi / 9)
        + 5 * np.cos(lat2d * np.pi / 45 + month)
        + np.random.normal(scale=1.5, size=lat2d.shape)
    )
    frames.append(sst)

# Color scaling
vmin = np.min(frames)
vmax = np.max(frames)
mapper = LinearColorMapper(palette="Turbo256", low=vmin, high=vmax)

# Initial frame
source = ColumnDataSource(data=dict(image=[frames[0]]))

Figure:

p = figure(
    x_range=(-180, 180), y_range=(-90, 90),
    width=800, height=400
)
p.image(
    image="image",
    x=-180, y=-90,
    dw=360, dh=180,
    color_mapper=mapper,
    source=source
)
color_bar = ColorBar(color_mapper=mapper)
p.add_layout(color_bar, 'right')

Title:

title = Title(text="Monthly SST - Jan")
p.add_layout(title, 'above')

# Button
button = Button(label="▶ Play", width=100)

# Convert frames to nested lists for JavaScript
frames_list = [f.tolist() for f in frames]

this is javascript section:

# Callback: escape all braces carefully
callback = CustomJS(args=dict(source=source, button=button, title=title), code=f"""
    if (!window._sst_anim) {{
        var frames = {frames_list};
        var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
        var current = 0;
        window._sst_anim = setInterval(function() {{
            current = (current + 1) % frames.length;
            source.data = Object.assign({{}}, source.data, {{image: [frames[current]]}});
            title.text = "Monthly SST - " + months[current];
        }}, 600);
        button.label = "⏸ Pause";
    }} else {{
        clearInterval(window._sst_anim);
        window._sst_anim = null;
        button.label = "▶ Play";
    }}
""")
button.js_on_click(callback)

Output:

output_file("sst_animation_working.html")
show(column(button, p))

Thank you for any help in advance


Solution

  • This is the issue:

    # Convert frames to nested lists for JavaScript*. 
    

    Bokeh / BokehJS has not accepted nested lists as "fake 2d" arrays for many years at this point. My suggestion is to put all the data for every month into the CDS up front (i.e. a column for every month, in addition to the image column that drives the glyph) so that the Python arrays will be serialized correctly as typed arrays with a shape on the JS side. Then use the CustomJS callback to copy the current month's (correctly serialized) column into the image column that drives the image glyph.

    Something like (untested)

    source.data.image = source.data[months[current]]
    

    It's possible you might also need a source.data = data afterward to "kick" Bokeh's event handler to notice the change and re-draw the plot.