periodic_callback using bokeh.client doesn't update image plot (bokeh 0.12.6)

Good day,

I have two figures with image plots that I am attempting to update with a periodic_callback on a bokeh.client session. I update the data_sources of the plots and verified that the data in the model is changing, however the changes are not rendered in the browser unless I refresh the page (or interestingly, if i open another tab and navigate to the same session URL, the image plots in both browser tabs are updated one time). Alternatively, if I generate a new image for each figure after updating the data sources, the plots in the browser are refreshed correctly at each interval, however I don’t believe this is proper or necessary.

Does anyone have any thoughts on what I am doing wrong? Below is the source code.

Regards,
Tom

from bokeh.client import push_session
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.plotting import figure, Document
from bokeh.layouts import row
import math
import random

mem_size = 1024 * 1024
disk_size = 1024 * 1024 * 8
mem_chunks = math.ceil(mem_size / 4096)
disk_chunks = math.ceil(disk_size / 4096)
scaling = 1
mem_ydim = int(math.ceil(math.sqrt(mem_chunks) / scaling))
mem_xdim = int(math.ceil(math.sqrt(mem_chunks) * scaling))
disk_ydim = int(math.ceil(math.sqrt(disk_chunks) / scaling))
disk_xdim = int(math.ceil(math.sqrt(disk_chunks) * scaling))

cmap = ['#bababa', '#404040', '#f4a582', '#ca0020']
mapper = LinearColorMapper(palette=cmap, low=1, high=4)

TOOLS = "wheel_zoom,save"

doc = Document()

a = [[1] * mem_xdim for i in range(mem_ydim)]
b = [[1] * disk_xdim for i in range(disk_ydim)]
p = figure(title="Memory Snapshot ({0}MB - {1} 4KB chunks)".format(mem_size / (1024*1024), mem_chunks),
           x_range=(0,mem_xdim), y_range=(0,mem_ydim), tools=TOOLS, toolbar_location='above')
p.axis.major_tick_line_color = None
p.axis.axis_line_color = None
p.xaxis.ticker = []
p.yaxis.ticker = []

p2 = figure(title="Disk Snapshot ({0}MB - {1} 4KB chunks)".format(disk_size / (1024*1024), disk_chunks),
           x_range=(0,disk_xdim), y_range=(0,disk_ydim), tools=TOOLS, toolbar_location='above')
p2.axis.major_tick_line_color = None
p2.axis.axis_line_color = None
p2.xaxis.ticker = []
p2.yaxis.ticker = []

session = push_session(doc, session_id='bokehtest')

def update():
    x = random.randint(0, int(math.ceil(math.sqrt(mem_chunks) / scaling)))
    y = random.randint(0, int(math.ceil(scaling * math.sqrt(mem_chunks))))
    a[x - 1][y - 1] += 1
    img.data_source.data.update({'columna': [a]})
    #p.image(image='columna', x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper, source=source)

    x = random.randint(0, int(math.ceil(math.sqrt(disk_chunks) / scaling)))
    y = random.randint(0, int(math.ceil(scaling * math.sqrt(disk_chunks))))
    b[x - 1][y - 1] += 1
    source2.data = dict(columnb=[b])
    #p2.image(image='columnb', x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper, source=source2)

doc.add_root(row(p, p2))

source = ColumnDataSource({'columna': [a]})
source2 = ColumnDataSource({'columnb': [b]})

img = p.image(name='img',image='columna', x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper,source=source)
img2 = p2.image(name='img2',image='columnb', x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper,source=source2)

doc.add_periodic_callback(update, 100)

session.show() # open the document in a browser

if __name__ == "__main__":
    doc.validate()
    session.loop_until_closed()

Hi,

You need to trigger a change in each CDS to update the plots. Try this…

from bokeh.client import push_session
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.plotting import figure, Document
from bokeh.layouts import row
import math
import random

mem_size = 1024 * 1024
disk_size = 1024 * 1024 * 8
mem_chunks = math.ceil(mem_size / 4096)
disk_chunks = math.ceil(disk_size / 4096)
scaling = 1
mem_ydim = int(math.ceil(math.sqrt(mem_chunks) / scaling))
mem_xdim = int(math.ceil(math.sqrt(mem_chunks) * scaling))
disk_ydim = int(math.ceil(math.sqrt(disk_chunks) / scaling))
disk_xdim = int(math.ceil(math.sqrt(disk_chunks) * scaling))

cmap = [’#bababa’, ‘#404040’, ‘#f4a582’, ‘#ca0020’]
mapper = LinearColorMapper(palette=cmap, low=1, high=4)

TOOLS = “wheel_zoom,save”

doc = Document()

a = [[1] * mem_xdim for i in range(mem_ydim)]
b = [[1] * disk_xdim for i in range(disk_ydim)]
p = figure(title=“Memory Snapshot ({0}MB - {1} 4KB chunks)”.format(mem_size / (1024*1024), mem_chunks),
x_range=(0,mem_xdim), y_range=(0,mem_ydim), tools=TOOLS, toolbar_location=‘above’)
p.axis.major_tick_line_color = None
p.axis.axis_line_color = None
p.xaxis.ticker =
p.yaxis.ticker =

p2 = figure(title=“Disk Snapshot ({0}MB - {1} 4KB chunks)”.format(disk_size / (1024*1024), disk_chunks),
x_range=(0,disk_xdim), y_range=(0,disk_ydim), tools=TOOLS, toolbar_location=‘above’)
p2.axis.major_tick_line_color = None
p2.axis.axis_line_color = None
p2.xaxis.ticker =
p2.yaxis.ticker =

session = push_session(doc, session_id=‘bokehtest’)

def update():
x = random.randint(0, int(math.ceil(math.sqrt(mem_chunks) / scaling)))
y = random.randint(0, int(math.ceil(scaling * math.sqrt(mem_chunks))))
data = source.data
data[‘columna’][0][x-1][y-1] += 1
source.trigger(‘data’, data, data)

a[x - 1][y - 1] += 1

img.data_source.data.update({‘columna’: [a]})

#p.image(image='columna', x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper, source=source)

x = random.randint(0, int(math.ceil(math.sqrt(disk_chunks) / scaling)))
y = random.randint(0, int(math.ceil(scaling * math.sqrt(disk_chunks))))
data = source2.data
data['columnb'][0][x-1][y-1] += 1
source2.trigger('data', data, data)

b[x - 1][y - 1] += 1

source2.data = dict(columnb=[b])

#p2.image(image='columnb', x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper, source=source2)

doc.add_root(row(p, p2))

source = ColumnDataSource({‘columna’: [a]})
source2 = ColumnDataSource({‘columnb’: [b]})

img = p.image(name=‘img’,image=‘columna’, x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper,source=source)
img2 = p2.image(name=‘img2’,image=‘columnb’, x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper,source=source2)

doc.add_periodic_callback(update, 100)

session.show() # open the document in a browser

if name == “main”:
doc.validate()
session.loop_until_closed()

Marcus,

Many thanks. This does work beautifully. It is curious why the trigger is not necessary in the example shown on the documentation for using bokeh.client (http://bokeh.pydata.org/en/latest/docs/user_guide/server.html#userguide-server-bokeh-client). Is it simply because the example updates the data elements x and y directly and does not use the source attribute?

Many thanks,
Tom

···

On Friday, June 30, 2017 at 5:23:29 AM UTC-4, Marcus Donnelly wrote:

Hi,

You need to trigger a change in each CDS to update the plots. Try this…

from bokeh.client import push_session
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.plotting import figure, Document
from bokeh.layouts import row
import math
import random

mem_size = 1024 * 1024
disk_size = 1024 * 1024 * 8
mem_chunks = math.ceil(mem_size / 4096)
disk_chunks = math.ceil(disk_size / 4096)
scaling = 1
mem_ydim = int(math.ceil(math.sqrt(mem_chunks) / scaling))
mem_xdim = int(math.ceil(math.sqrt(mem_chunks) * scaling))
disk_ydim = int(math.ceil(math.sqrt(disk_chunks) / scaling))
disk_xdim = int(math.ceil(math.sqrt(disk_chunks) * scaling))

cmap = [‘#bababa’, ‘#404040’, ‘#f4a582’, ‘#ca0020’]
mapper = LinearColorMapper(palette=cmap, low=1, high=4)

TOOLS = “wheel_zoom,save”

doc = Document()

a = [[1] * mem_xdim for i in range(mem_ydim)]
b = [[1] * disk_xdim for i in range(disk_ydim)]
p = figure(title=“Memory Snapshot ({0}MB - {1} 4KB chunks)”.format(mem_size / (1024*1024), mem_chunks),
x_range=(0,mem_xdim), y_range=(0,mem_ydim), tools=TOOLS, toolbar_location=‘above’)
p.axis.major_tick_line_color = None
p.axis.axis_line_color = None
p.xaxis.ticker =
p.yaxis.ticker =

p2 = figure(title=“Disk Snapshot ({0}MB - {1} 4KB chunks)”.format(disk_size / (1024*1024), disk_chunks),
x_range=(0,disk_xdim), y_range=(0,disk_ydim), tools=TOOLS, toolbar_location=‘above’)
p2.axis.major_tick_line_color = None
p2.axis.axis_line_color = None
p2.xaxis.ticker =
p2.yaxis.ticker =

session = push_session(doc, session_id=‘bokehtest’)

def update():
x = random.randint(0, int(math.ceil(math.sqrt(mem_chunks) / scaling)))
y = random.randint(0, int(math.ceil(scaling * math.sqrt(mem_chunks))))
data = source.data
data[‘columna’][0][x-1][y-1] += 1
source.trigger(‘data’, data, data)

a[x - 1][y - 1] += 1

img.data_source.data.update({‘columna’: [a]})

#p.image(image='columna', x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper, source=source)

x = random.randint(0, int(math.ceil(math.sqrt(disk_chunks) / scaling)))
y = random.randint(0, int(math.ceil(scaling * math.sqrt(disk_chunks))))
data = source2.data
data['columnb'][0][x-1][y-1] += 1
source2.trigger('data', data, data)

b[x - 1][y - 1] += 1

source2.data = dict(columnb=[b])

#p2.image(image='columnb', x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper, source=source2)

doc.add_root(row(p, p2))

source = ColumnDataSource({‘columna’: [a]})
source2 = ColumnDataSource({‘columnb’: [b]})

img = p.image(name=‘img’,image=‘columna’, x=0, y=0, dw=mem_xdim, dh=mem_ydim, color_mapper=mapper,source=source)
img2 = p2.image(name=‘img2’,image=‘columnb’, x=0, y=0, dw=disk_xdim, dh=disk_ydim, color_mapper=mapper,source=source2)

doc.add_periodic_callback(update, 100)

session.show() # open the document in a browser

if name == “main”:
doc.validate()
session.loop_until_closed()

Hi Tom,

I’m not actually sure why that’s the case - I just know you normally need to do it! Note that a CDS is still being used in the example - there’s one automatically created for the plot data (accessed using r2.data_source). It might be that you need to trigger an update if you create your own source and use it for plots as opposed to using the automatically created one.

BTW - looking at that example, do you (or anyone) know where the ‘step’ variable comes from? I can’t work it out!

Marcus.

Marcus,

I believe this is because of the @cosine decorator that is attached to the update function. If you were to remove that decoration, you would see that the compiler would balk at the fact that update is supposed to take exactly 1 argument and you provided 0 (because the @cosine decorator is responsible for generating the argument to update)

See the following reference for the definition of the cosine decorator:

···

On Friday, June 30, 2017 at 9:17:17 AM UTC-4, Marcus Donnelly wrote:

Hi Tom,

I’m not actually sure why that’s the case - I just know you normally need to do it! Note that a CDS is still being used in the example - there’s one automatically created for the plot data (accessed using r2.data_source). It might be that you need to trigger an update if you create your own source and use it for plots as opposed to using the automatically created one.

BTW - looking at that example, do you (or anyone) know where the ‘step’ variable comes from? I can’t work it out!

Marcus.

Ah, that makes sense, I’ve just looked at the @cosine decorator code and seen how it works. Thanks!