Search code examples
pythondata-visualizationbokeh

Bokeh add arrow dynamically


I am new to using Bokeh. For my project, I am trying to use bokeh to make arrows from one point to the next. So I am making the points by double-clicking and then drawing the arrows by a single click. But it doesn't seem to do anything.

from bokeh.models import Arrow, OpenHead
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.io import curdoc
from bokeh.events import DoubleTap, Tap

coordList=[]
source = ColumnDataSource(data=dict(x=[], y=[]))

#add a dot where the click happened
def callback(event):
    Coords=(event.x,event.y)
    coordList.append(Coords)
    source.data = dict(x=[i[0] for i in coordList], y=[i[1] for i in coordList])
    for x, y in coordList:
        if x == None and y ==  None:
            coordList.pop(0)
p = figure(plot_width=700, plot_height=700)

def draw(event):
    # Function to add arrows from the coordList
    p.add_layout(Arrow(end=OpenHead(line_color="firebrick", line_width=4),
                   x_start=1, y_start=1, x_end=4, y_end=4))

p.circle(source=source,x='x',y='y')
p.on_event(DoubleTap, callback)
p.on_event(Tap, draw)

curdoc().add_root(p)

Any help would be appreciated. Thanks


Solution

  • Yeah, it's a bug: https://github.com/bokeh/bokeh/issues/8862

    See my comments inline.

    from bokeh.events import DoubleTap, Tap
    from bokeh.io import curdoc
    from bokeh.models import Arrow, OpenHead, CustomJS
    from bokeh.models import ColumnDataSource
    from bokeh.plotting import figure
    
    p = figure(plot_width=700, plot_height=700)
    # We need to have at least one renderer present for the plot
    # to be able to compute the initial ranges. Otherwise, the very
    # first double tap event will have no coordinates.
    bogus_renderer = p.circle(x=[0], y=[0], fill_alpha=0, line_alpha=0)
    
    # This is needed to make sure that PlotView recalculates
    # all renderers' views when we call `add_layout`.
    p.js_on_change('center', CustomJS(code='cb_obj.properties.renderers.change.emit();'))
    
    source = ColumnDataSource(data=dict(x=[], y=[]))
    
    
    def callback(event):
        # Removing the renderer to avoid messing with DataRange1d.
        bogus_renderer.data_source.data = dict(x=[], y=[])
        source.stream(dict(x=[event.x], y=[event.y]))
    
    
    def draw(event):
        if source.data['x']:
            last_dot_x = source.data['x'][-1]
            lalt_dot_y = source.data['y'][-1]
            p.add_layout(Arrow(end=OpenHead(line_color="firebrick", line_width=4),
                               x_start=last_dot_x, y_start=lalt_dot_y, x_end=event.x, y_end=event.y))
    
    
    p.circle(source=source, x='x', y='y')
    p.on_event(DoubleTap, callback)
    p.on_event(Tap, draw)
    
    curdoc().add_root(p)