I’ve found a way to do the custom UI interactions but improving the performance so this wasn’t necessary would be much better. I’ve tried to pull all our Bokeh code together into a single function and generate some phoney data. Code below. When I run this and drag the events left it takes about 5s to redraw (I’m using 2.4.3).
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(plot_width=615, plot_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 indices = [];
let select = null;
// filter out events with a non-visible (i.e. white) status colour (used for unknown/unsupported statuses)
for (let i = 0, numEvents = source.get_length(); i < numEvents; i++){
select = source.data['StatusColor'][i] !== '#ffffff';
indices.push(select);
}
return indices;
"""
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(source=data_source, filters=[status_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(source=data_source, filters=[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')
# allow 'gliding' if plot cannot continue zooming out focused on the mouse position
maintain_focus = False
# increase wheel zoom speed from default (1/600) - zooms faster on Chrome & Edge than FF
speed = 2 / 600
zoom_tools = [WheelZoomTool(maintain_focus=maintain_focus, speed=speed, 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