Search code examples
pythonplotbokehglyph

Bokeh plot using a function of values in ColumnDataSource


I'd like to plot a glyph using a function of the values in a ColumnDataSource instead of the raw values.

As a minimum example, suppose I want to plot a point that the user can drag, which moves another point around. The position of the second point is some arbitrary function of the position of the first point.

I can draw the second point right on top of the first point like so:

from bokeh.plotting import figure, show
from bokeh.models import PointDrawTool, ColumnDataSource, Circle

p = figure(x_range=(0, 10), y_range=(0, 10), tools=[],
           title='Point Draw Tool')

source = ColumnDataSource({
    'x': [1], 'y': [1], 'color': ['red']
})

# plot a point at x,y
renderer = p.scatter(x='x', y='y', source=source, color='color', size=10)

# create a circle at the same position as the point
glyph = Circle(x='x',y='y', size=30)
p.add_glyph(source, glyph)

# allow the user to move the point at x,y
draw_tool = PointDrawTool(renderers=[renderer], empty_value='black')
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool

show(p)

This plot draws the second point with exactly the same (x,y) coordinates as the first point.

Now, suppose I'd like to draw the second point at some arbitrary function of the first point (f(x,y), g(x,y)) for a pair of arbitrary functions f() and g(). To make it simple, let's say the coordinates of the new point are (2x, 2y) .

Can someone tell me how to do this?

I'd like to do something like

glyph = Circle(x=2*'x',y=2*'y', size=30)
p.add_glyph(source, glyph)

Clearly this is not how to do it, but hopefully this makes it clear what I'm trying to do. In general, I'd like to do something like:

glyph = Circle(x=f('x','y'), y=g('x','y'), size=30)
p.add_glyph(source, glyph)

I have tried something silly like the following:

def get_circle(x,y):
   new_x = 2*x
   new_y = 2*y
   return Circle(x=new_x, y=new_y, size=30)

Solution

  • You can create a ColumnDataSource and use a CustomJS to apply changes to your data.

    In the example below I create a default source, add two renderers to a figure, define a JavaScript callback and excecute the callback every time the source changes. Because I link the only one renderer to the PointDrawTool, the second renderer is updated, after I moved the frist renderer.

    Minimal Example

    from bokeh.plotting import figure, show, output_notebook
    from bokeh.models import CustomJS, ColumnDataSource, PointDrawTool
    output_notebook()
    
    source = ColumnDataSource(dict(
        x1=[1,3],
        y1=[2,3],
        x2=[2,6],
        y2=[6,9]
    ))
    
    
    p = figure(width=300, height=300)
    r1 = p.circle(x='x1', y='y1', color='blue', source=source, size=5)
    r2 = p.triangle(x='x2', y='y2', color='green', source=source, size=5)
    
    draw_tool = PointDrawTool(renderers=[r1], empty_value='black')
    p.add_tools(draw_tool)
    
    callback = CustomJS(args=dict(source=source),
        code="""
        function f(x) {
          return x*2;
        };
        function g(y) {
          return y*3;
        };
    
        let data = source.data
        data['x2'] = data['x1'].map(f)
        data['y2'] = data['y1'].map(g)
    
        source.change.emit()
        """
    )
    
    source.js_on_change('data', callback)
    show(p)
    

    Result

    updating a renderer by other changed renderer

    Comment

    The example can have even more cirlces and triangles but at the moment only one can be moved and updated at a time and the order matters.

    Really hope this helps you.