Search code examples
pythonbokeh

Bokeh: disable Auto-ranging while using Edit Tools


I've included the PolyDrawTool in my Bokeh plot to let users circle points. When a user draws a line near the edge of the plot the tool expands the axes which often messes up the shape. Is there a way to freeze the axes while a user is drawing on the plot?

I'm using bokeh 1.3.4

MRE:

import numpy as np
import pandas as pd
import string

from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, LabelSet
from bokeh.models import PolyDrawTool, MultiLine


def prepare_plot():
    embedding_df = pd.DataFrame(np.random.random((100, 2)), columns=['x', 'y'])
    embedding_df['word'] = embedding_df.apply(lambda x: ''.join(np.random.choice(list(string.ascii_lowercase), (8,))), axis=1)

    # Plot preparation configuration Data source
    source = ColumnDataSource(ColumnDataSource.from_df(embedding_df))
    labels = LabelSet(x="x", y="y", text="word", y_offset=-10,x_offset = 5,
                      text_font_size="10pt", text_color="#555555",
                      source=source, text_align='center')
    plot = figure(plot_width=1000, plot_height=500, active_scroll="wheel_zoom",
                      tools='pan, box_select, wheel_zoom, save, reset')

    # Configure free-hand draw
    draw_source = ColumnDataSource(data={'xs': [], 'ys': [], 'color': []})
    renderer = plot.multi_line('xs', 'ys', line_width=5, alpha=0.4, color='color', source=draw_source)
    renderer.selection_glyph = MultiLine(line_color='color', line_width=5, line_alpha=0.8)
    draw_tool = PolyDrawTool(renderers=[renderer], empty_value='red')
    plot.add_tools(draw_tool)

    # Add the data and labels to plot
    plot.circle("x", "y", size=0, source=source, line_color="black", fill_alpha=0.8)
    plot.add_layout(labels)
    return plot


if __name__ == '__main__':
    plot = prepare_plot()
    show(plot)

GIF of the issue


Solution

  • The PolyDrawTool actually updates a ColumnDataSource to drive a glyph that draws what the users indicates. The behavior you are seeing is a natural consequence of that fact, combined with Bokeh's default auto-ranging DataRange1d (which by default also consider every glyph when computing the auto-bounds). So, you have two options:

    • Don't use DataRange1d at all, e.g. you can provide fixed axis bounds when you call figure:

      p = figure(..., x_range=(0,10), y_range=(-20, 20)
      

      or you can set them after the fact:

      p.x_range = Range1d(0, 10)
      p.y_range = Range1d(-20, 20)
      

      Of course, with this approach you will no longer get any auto-ranging at all; you will need to set the axis ranges to exactly the start/end that you want.

    • Make DataRange1d be more selective by explicitly setting its renderers property:

      r = p.circle(...)
      p.x_range.renderers = [r] 
      p.y_range.renderers = [r] 
      

      Now the DataRange models will only consider the circle renderer when computing the auto-ranged start/end.