Timeseries x-axis drifting over time

I have multiple timeseries line plots that I update on an fixed interval (200ms periodic callback, using stream() api).

All figures share the same x-axis:

# create shared x_range and display the past 20 seconds
x_range = DataRange1d()
x_range.follow = "end"
x_range.follow_interval = 1000 * 20 # 20 seconds
x_range.range_padding_units = "percent"
x_range.range_padding = 0.01 # 1% padding
figure1 = figure(width=950, height=600,syncable=False, x_axis_type='datetime', y_axis_location = "right")
if x_range is not None:
    figure1.x_range = x_range

# repeated for each plot

The goal is to display the data of the past 20 seconds. The issue is that the x-axis drifts over time as you can see in the following image:

Streaming looks like this:

data = {"x": [datetime.datetime.now()], "tilt": [alpha]}
source.stream(data, rollover=roll_over_amount)

All plots have different rollover and the frequency of new data varies.

How can I prevent the x-axis drift?

Update: If I set the rollover to 2 then there is no drifting. Rollover in my code varies between 100 and 200 and update frequency is between 1Hz and 10Hz. (-> Frequency is dynamic and not in my control so I cant calculate a rollover window that fits 20 seconds of data for all plots)

I haven’t ever seen this, but also haven’t played with the follow_interval stuff much in years. It’s not really possible to speculate without a complete Minimal Reproducible Example to actually run and investigate.

It seems to have something to do with the different rollover values. Here is a minimal reproducible example:

import datetime
import random
from bokeh.models import ColumnDataSource, DataRange1d, DatetimeTickFormatter
from bokeh.plotting import figure
from bokeh.plotting import curdoc
from bokeh.layouts import gridplot

doc = curdoc()
rnd = random.Random()

# create shared x_range and display the past 10 seconds
x_range = DataRange1d()
x_range.follow = "end"
x_range.follow_interval = 1000 * 10 # 10 seconds
x_range.range_padding_units = "percent"
x_range.range_padding = 0.01 # 1% padding

datetime_tick_formatter = DatetimeTickFormatter(microseconds = "%H:%M:%S", milliseconds = "%H:%M:%S", seconds = "%H:%M:%S", minutes = "%H:%M:%S", hours = "%H:%M:%S", days="%H:%M:%S",months="%H:%M:%S", years="%H:%M:%S",  minsec = "%H:%M:%S" )

figure1 = figure(width=950, height=600, syncable=False, x_axis_type='datetime')
figure2 = figure(width=950, height=600, syncable=False, x_axis_type='datetime')

source1 = ColumnDataSource({"x": [], "value1": []})
source2 = ColumnDataSource({"x": [], "value2": []})

figure1.line("x", "value1", syncable=False, source=source1, legend_label=f"value1")
figure1.scatter("x", "value1", syncable=False, source=source1, legend_label=f"value1")
figure1.xaxis.formatter = datetime_tick_formatter
figure1.x_range = x_range

figure2.line("x", "value2", syncable=False, source=source2, legend_label=f"value2")
figure2.scatter("x", "value2", syncable=False, source=source2, legend_label=f"value2")
figure2.xaxis.formatter = datetime_tick_formatter
figure2.x_range = x_range

def update():
    timestamp = datetime.datetime.now()
    source1.stream({"x": [timestamp], "value1": [1]}, rollover=200)

    # source 2 has a 30% chance to update
    if rnd.random() > 0.7:
        source2.stream({"x": [timestamp], "value2": [1]}, rollover=800)

plots_grid = gridplot(
    [
        [figure1, figure2]
    ],
    toolbar_location="right",
    toolbar_options=dict(),
    sizing_mode="stretch_both",
)

doc.add_root(plots_grid)
doc.add_periodic_callback(update, 50)

I could “fix” this drifting by choosing a rollover value so that for example rollover = 200 / ( 10 / update_rate) meaning that each plot contains data worth of 20 seconds. But like I mentioned I do not have control over the data update frequency. But it would be much better to have a reliable “past XX seconds” x-axis

I’ll have to try out the example directly later, but I should clarify one thing up front. The rollover value has nothing do to with time, it is only a measure of the number of points / “rows” of the CDS to retain after an update. A “past XX seconds” option does not currently exist.

There was, quite some time ago, a brief discussion of adding a callback option to afford more sophisticated rollover computation on the fly:

ColumnDataSource.stream with callable to specify rollover policy · Issue #7024 · bokeh/bokeh · GitHub

But there was never a ton of interest expressed for it, and since then, it has not gotten any attention amidst other priorities.