X-Axis reversing

I’m having the problem with the x-axis on my figure reversing. See attached code. To reproduce

  1. Pan until x=800 in fairly central
  2. Select Wheel Zoom.
  3. Zoom in really quickly.

x-axis reverses to values are in decending order. Zoom out and they are still in decending order.

Doesn’t happen all the time, but probably 50% of the time for me.

What is wrong?

Picture

Code

from bokeh.core.properties import value
from bokeh.layouts import column
from bokeh.models import Legend, LegendItem
from bokeh.plotting import figure, curdoc
from bokeh.sampledata.autompg import autompg
from bokeh.transform import jitter

from bokeh.models import ColumnDataSource, FactorRange, Range1d, CustomJSFilter, GroupFilter, CDSView, LegendItem, \
    WheelZoomTool, BoxZoomTool, ZoomOutTool, ZoomInTool, PanTool, UndoTool, ResetTool, Legend
from bokeh.plotting import figure


def setup_figure():

    streams = ['Wi-Fi STA', 'Wi-Fi', 'Radio', 'Errors']
    event_types = ['Errors:Fault', 'Errors:Lost debug', 'Radio:Trim', 'Wi-Fi STA:Add vif', 'Wi-Fi STA:Del vif',
                   'Wi-Fi STA:Roam', 'Wi-Fi STA:STA Connect', 'Wi-Fi STA:STA Disconnect', 'Wi-Fi:Scan']

    status_colors = ['#e6194b', '#3cb44b']
    event_colors = ("#4363d8", "#f58231", "#911eb4", "#42d4f4", "#f032e6", "#bfef45", "#fabed4", "#469990", "#dcbeff")
    event_props = {'Duration': [], 'TimeStart': [], 'TimeEnd': [], 'TimeMid': [], 'SignalID': [], 'EventType': [],
                   'Details': [], 'VerboseDetails': [], 'EventGroup': [],
                   'EventColor': [], 'StatusColor': []}

    # Generate some data
    start_time = 0
    duration = 0.01
    for i in range(200000):
        event_props['SignalID'].append(i)
        event_props['Duration'].append(duration)
        event_props['TimeStart'].append(start_time)
        event_props['TimeEnd'].append(start_time + duration)
        event_props['TimeMid'].append(start_time + duration / 2)
        start_time += duration

        event_type = event_types[i % len(event_types)]
        stream, _, _ = event_type.partition(":")
        event_props['EventType'].append(event_type)
        event_props['Details'].append(event_type)
        event_props['VerboseDetails'].append(event_type)
        event_props['EventGroup'].append(stream)
        event_props['EventColor'].append(event_colors[i % len(event_colors)])
        event_props['StatusColor'].append(status_colors[i % len(status_colors)])

    end_time = start_time
    data_source = ColumnDataSource(data=event_props, name="timelineDataSource")

    y_range = FactorRange(factors=streams[::-1])
    x_range = Range1d(start=0, end=end_time / 4,
                      bounds=(-0.5, end_time + 0.5),
                      min_interval=0.0001,
                      max_interval=end_time / 2)

    fig = figure(width=615, height=400, sizing_mode='stretch_width',
                 y_range=y_range, x_range=x_range, name='timeline', lod_factor=10,
                 lod_threshold=1000, lod_timeout=100, output_backend='webgl'
                 )

    x_axis = fig.xaxis[0]
    x_axis.formatter.power_limit_high = 7
    x_axis.formatter.power_limit_low = -7
    x_axis.axis_label = "Time..."

    inline_js = """
        const N = source.get_length()
        const indices = new Array(N);
        const colors = source.data['StatusColor']
        for (let i = 0; i < N; i++) {
            indices[i] = (colors[i] !== '#ffffff')
        }
    """

    status_filter = CustomJSFilter(code=inline_js)

    status_renderers = []
    event_renderers = []
    legend_items = []

    for event_type in event_types:
        # create filtered views to select events of just this type from the column data source
        # (filters are computed in the browser by Bokeh)
        event_filter = GroupFilter(column_name='EventType', group=event_type)

        # plot taller boxes in status colour underneath boxes in event colour
        # this creates a top-bottom border for events with the status colour when rendered

        # get events of just this type which have a known status
        event_status_view = CDSView()
        event_status_view.filter &= status_filter
        event_status_view.filter &= event_filter
        r_status = fig.rect(x='TimeMid', y='EventGroup', width='Duration',
                            height=0.8,
                            line_color='StatusColor', fill_color='StatusColor', alpha=1,
                            source=data_source, view=event_status_view, dilate=True)

        event_view = CDSView(filter=event_filter)
        r_event = fig.rect(x='TimeMid', y='EventGroup', width='Duration', height=0.7,
                           line_color='EventColor', fill_color='EventColor', alpha=1,
                           source=data_source, view=event_view, dilate=True)

        legend_items.append(LegendItem(label=event_type, renderers=[r_status, r_event]))
        status_renderers.append(r_status)
        event_renderers.append(r_event)

    legend = Legend(items=legend_items, click_policy="hide", title='Click to hide/show events',
                    name=f'{fig.name}Legend0', visible=True)

    fig.add_layout(legend, 'right')

    zoom_tools = [WheelZoomTool(dimensions="width"),
                  BoxZoomTool(dimensions="width"), BoxZoomTool(),
                  ZoomOutTool(dimensions="width"), ZoomInTool(dimensions="width")]
    pan_tools = [PanTool(dimensions="width"), PanTool(dimensions="height"), PanTool()]
    tools = [UndoTool(), ResetTool(), *pan_tools, *zoom_tools]

    fig.tools = tools
    fig.toolbar_location = "above"
    fig.toolbar.active_drag = pan_tools[0]

    return fig

p1 = setup_figure()

# add column - need to run this with bokeh --serve
curdoc().add_root(column(p1, sizing_mode='stretch_width'))

Seems like a bug, I don’t have any other immediate suggestions except to file a GitHub Issue with full details. Before that I strongly encourage you to pare down the MRE code, removing anything not directly related or necessary to reproducing the issue. Especially try to determine if the problem occurs with a smaller set of configuration options to Range1d or if it only happens when all of those are present.