How speed up zoom/pan in layouts with many plots

I am trying to make a dashboard that displays multiple plots simultaneously. My problem is that each time I wheel zoom or pan, BokehJS executes compute_layout, which starts at 10-20 ms duration for a single plot but increases to 400ms for 30 plots, and it gets worse if I add extra layout elements. Do you have any suggestions or techniques I could use to improve the user experience?

To reproduce, I recommend setting the env var and opening the JavaScript console in the browser
export BOKEH_LOG_LEVEL=debug

from bokeh.layouts import gridplot
from bokeh.models import  ColumnDataSource
from bokeh.plotting import figure, curdoc
from bokeh.plotting import figure

import pandas as pd
import numpy as np

N = 1000

def make_data(N):
   data = pd.DataFrame(np.random.random(N*2).reshape(N, int(2)), columns=['x', 'y'])
   return ColumnDataSource(data)

   plots = []
   for i in range(50):
       source = make_data(N)

       p = figure(title=f'Figure {i}', tools=['wheel_zoom', 'pan'], active_scroll="wheel_zoom", width=300, height=300)
       p.circle(x='x', y='y', source=source)
       plots.append(p)
   gp = gridplot(list(zip(plots[::2], plots[1::2])))
   curdoc().add_root(gp)

Note: I am aware of people having problems loading large layouts Browser rendering extremely slow when many figures in a gridplot · Issue #6294 · bokeh/bokeh · GitHub. In this case, loading the page is not an issue.

1 Like

Playing around some more, I figured out that what is triggering the layout recomputation is that while zooming, the axis labels change. Setting p.axis.visible = False makes the zooming and panning incredibly smooth. I wonder if there is a way to disable the axis update while panning/zooming.

Not really, but you might try increasing the general min_border (or more specific min_border_left, etc) values of the plots to be larger, so that the border space for the axes does not need to grow or shrink when the tick label sizes change. Or, if the axes can/should be shared between plots (i.e. is this a SPLOM?) then you can omit the interior axes as is done in this example: bokeh/iris_splom.py at branch-2.4 · bokeh/bokeh · GitHub

FYI the code above is plain-text quoted, to apply code formatting use either the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks. I’ve edited the post.

Thanks for fixing the post.

Unfortunately, it seems that increasing min_border or min_border_left has no effect. I suspect that some property changes and then automatically triggers a layout recomputation from the very root of the document, irrespective of whether it needs to.

Ok. Having probably spent way too much time on this, I think I figured it out. The trick is in adjusting the ticks to change in width less often. For example, adding the following allows to zoom and pan within reasonable ranges without triggering any layout recomputations. There might be a format for which they are never triggered, but I have not found it.

 p.xaxis[0].formatter = PrintfTickFormatter(format="%1.3f")
 p.yaxis[0].formatter = PrintfTickFormatter(format="%1.3f")

I love Bokeh and I am glad to see that with a some of these these tips & tricks it becomes possible to make much more ambitious applications.

2 Likes

Thanks for the kind words @rsdenijs1 BTW I guess some of these things come down to platform differences. FWIW I finally ran the original code on OSX/Safari and it seemed very snappy on my laptop.

Edit: @rsdenijs1 I guess it’s also worth asking what version you are using? Some older versions had very bad layout performance. I was running with the latest 2.3

One more update on this. Precisely, the layout is updated when the width of the characters used in the ticks increases/decreases. For example, that can be because the max range for x goes from 9 to 10 or because it goes from 0 to -1. But I found out that because the default font in Bokeh is a proportional font, the total width may still change even if the characters stays the same. So I recommend switching to a monospaced font for more consistent behavior.

With these example settings, no layout will need to be recomputed as long as the x_range and y_range stay within (-100, 1000). It is incredibly smooth and pleasant even with thousands of points and over 30 plots. Numbers within these ranges can be displayed with 6 characters. If you zoom/pan out farther, 7 characters will be needed for the ticks, e.g. -100.00. If you know the range of your data beforehand, you can set the correct width/precision in the formatting and appropriate bounds for the ranges, so the user can not accidentally scroll beyond that.

 p.xaxis.major_label_text_font = 'courier'
 p.yaxis.major_label_text_font = 'courier'

 p.xaxis[0].formatter = PrintfTickFormatter(format="%6.2f")
 p.yaxis[0].formatter = PrintfTickFormatter(format="%6.2f")
1 Like

@Bryan I am on Bokeh 2.3.0 & OSX Big Sur. Interesting that you got better timing. If you enable debugging in BokehJS it logs to console each time the layout is recomputed with the timing.

Also FWIW the plan for eventual 3.0 is to remove most/all of our custom layout computations for everything outside the canvas, and “get out of the way” of using Bokeh with other tools for layout . We had high ambitions to enable lots of fancy data-informed layout features, but ultimately, trying to compete with optimized browser layout engines is just a losing proposition.

2 Likes