Dynamic X range for scatter plot

Hi I was able to get my data to stream to a bokeh server but I’m noticing that the xaxis does not dynamically update with it.

Here’s my current test code:

from random import randint

from bokeh.layouts import column
from bokeh.models import Button,ColumnDataSource,FactorRange
from bokeh.plotting import figure, curdoc

# create a plot and style its properties
p = figure(x_range=(0,250), y_range=(9230, 9240), toolbar_location=None)
ds = ColumnDataSource(data=dict(count=[], l4=[]))

# render scatter plot
r = p.circle(x='count', y='l4', source=ds)

i = 0



# create a callback that will get "l4" data for the next count
def button_callback():
    curdoc().add_periodic_callback(update, 20)

def update():
    global i
    new_data = stream_l4_data()
    i = i+1
    ds.stream(new_data)

def stream_l4_data() -> dict:
    global i
    return dict(count=[i+1], l4=[randint(9232, 9235)])

# add a button widget and configure with the call back
button = Button(label="Start")
button.on_click(button_callback)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(button, p))

I’m aware I have x_range=(0, 250) and that its limiting my graph, but I haven’t been found anything in the forums or the documentation that allows me to grow the xaxis dynamically as more data is put in. The width will stay the same, so I can possibly shrink the tick mark spacing as time goes on, but the beginning mark of the x axis should be 0 and the last mark should be the final count of how much data has passed though (0 based).

The question is a little confusing. If you want the bounds of the axis to update automatically based on the data, you should omit the x_range=(0, 250) so that the default auto-updating range is used. Is the default auto-updating range doing something you don’t want?

If you mean you want the axis to grow physically, i.e. take up more pixels/space on the screen, there is nothing to make that happen automatically.

No I think I seem to have misread the documentation or something was not clear to me. I believe the reason I was not doing that was because when the test page loaded with the graph, I get a blank screen until I hit the button to start the graph with the callback.

Maybe I need to dig further down the documentation, but is there a way to show the axes before starting the call backs?

It’s not about the callbacks, it’s about the fact that there is no data to start. The DataRange determines the bounds automatically based on the data. If there is no data, then there are no bounds set. If there are no bounds set, the axis can’t render.

You can either:

  • Make sure at least one data point is there to start, before the callbacks start adding more, or
  • Add some invisible (i.e. alpha=0) glyph up front, just to give the DataRange something to work with.

Thanks for the help!

Last question, I can open up a new thread if needed but I’m trying to remove the periodic call back. I did some searching and found your post a little while back on removing callbacks from a document.

Here’s my new test code using that source as a baseline to remove a callback as soon as the generator is exhausted:

from random import randint
from collections import Counter

from bokeh.layouts import column
from bokeh.models import Button, ColumnDataSource, FactorRange
from bokeh.driving import count
from bokeh.plotting import figure, curdoc

# create a plot and style its properties
p = figure()
ds = ColumnDataSource(data=dict(count=[], l4=[]))

# add a text renderer to our plot (no data yet)
r = p.circle(x='count', y='l4', source=ds)

i = 0
data_amount = 250

L4_test = (randint(9232, 9235) for _ in range(data_amount))


# create a callback that will get "l4" data for the next count
def button_callback():
    curdoc().add_periodic_callback(update, 20)

def update():
    global i
    global data_amount
    if i == data_amount:
        curdoc().remove_periodic_callback(update)
        return
    new_data = stream_l4_data()
    ds.stream(new_data)
    i = i + 1


def stream_l4_data() -> dict:
    global i
    return dict(count=[i + 1], l4=[next(L4_test)])



# add a button widget and configure with the call back
button = Button(label="Start")
button.on_click(button_callback)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(button, p))

When I run this snippet using bokeh serve test.py it runs as expected until the generator exhausts. When the generator exhausts the remove_periodic_callback(update) function runs, but now my terminal shows ValueError: callback already ran or was already removed, cannot be removed again. How is the update function still running in the event loop when I specifically turned it off?

How is the update function still running in the event loop when I specifically turned it off ?

Well, this is ultimately a Tornado question, since these adding and removing callbacks are fairly thin wrappers around Tornado APIs. But “remove callback” might technically mean something specific like “don’t schedule any more”, and there may already be a next one scheduled even when the current one starts. Typically remove_periodic_callback is called from “outside” e.g. a button to turn off, not a callback turning itself off, so this is never a worry in that situation. In any case this is evidently just a race condition you will have to handle. Offhand, a try / except that ignores a ValueError is probably the simplest thing to do.

Okay, thanks for the info, I was able to use a try/except to remove the error spam.

1 Like