Interactive Christmas Tree in Bokeh

This is a simple demonstration of interactivity in Jupyter notebooks using the Bokeh plotting library.


In [1]:
import numpy as np
from ipykernel.comm import Comm
import bokeh
from bokeh.plotting import figure, show
from bokeh.models import Range1d, ColumnDataSource
from bokeh.io import output_notebook
from ipywidgets import interact
output_notebook()


BokehJS successfully loaded.

Updating Bokeh Datasources

Jupyter Notebook has a mechanism called comm channels. Essentially you define a function in a require.js module, and target it with a Comm instance.

In bokeh_update.js there is a function to replace a bokeh data source, so let's setup the channel:


In [2]:
%%html
<script src="bokeh_update.js"></script>



In [3]:
update_bokeh_comm = Comm(target_name='bokeh_update_target',
            target_module='bokeh_update', data={})

def replace_bokeh_data_source(ds):
    update_bokeh_comm.send({"custom_type": "replace_bokeh_data_source",
            "ds_id": ds.ref['id'], # Model Id
            "ds_model": ds.ref['type'], # Collection Type
            "ds_json": bokeh.protocol.serialize_json(ds.vm_serialize())
    })

Prepare Datasources

In order to update the datasources of an existing plot, it's best to create the plot with specific datasource objects from the beginning.


In [4]:
def compute_tree(levels=6, slope = 0.4, stem_width=0.2, baubles_n=20):
    # Base shape of a tree level, parabolic slope
    shape_x = np.linspace(-1,1,15)
    shape_y = (shape_x>=0)*(shape_x-1)**2 + (shape_x<0)*(shape_x+1)**2
    
    patch_colors = []
    patch_x = []
    patch_y = []

    # Make a simple stem
    patch_x.append([0,stem_width,-stem_width])
    patch_y.append([0,-6,-6])
    patch_colors.append("brown")
    
    # Loop through the tree levels
    for level in range(levels,0,-1):
        width = level*slope
        patch_x.append(shape_x*width)
        patch_y.append(shape_y *5/levels - (level / levels*5))
        patch_colors.append("green")
    
    # Generate Baubles
    #
    # compensate for different level widths
    probs = np.arange(0,levels+1)
    probs = probs / np.sum(probs)
    # draw random levels, according to the width
    baubles_levels = np.random.choice(np.arange(0,levels+1), baubles_n, p=probs)
    # x coordinates spread out, according to the levels
    baubles_x = (np.random.uniform(-1,1,baubles_n) * baubles_levels * slope) *10 // 4 * 4 / 10 + np.random.normal(0,0.015,baubles_n)
    # compute y coordinates from levels
    baubles_y = -(baubles_levels / levels*5) - 0.2 + np.random.normal(0,0.015,baubles_n)
    
    baubles_colors =["red"]*baubles_n
    baubles_radius = np.ones(baubles_n)*0.1
    return patch_x, patch_y, patch_colors, baubles_x, baubles_y, baubles_radius, baubles_colors

patch_x, patch_y, patch_colors, baubles_x, baubles_y, baubles_radius, baubles_color = compute_tree()
branches_ds = ColumnDataSource(data=dict(patch_x=patch_x, patch_y=patch_y, color=patch_colors))
baubles_ds = ColumnDataSource(data=dict(x=baubles_x, y=baubles_y, radius=baubles_radius, color=baubles_color))

In [5]:
def plot_tree():
    p = figure(plot_width=800, plot_height=400)
    p.y_range = Range1d(-6,1)
    p.x_range = Range1d(-6,6)
    p.patches(xs="patch_x",ys="patch_y",
              color="color", source=branches_ds, alpha=0.9, line_width=2)

    p.circle(x="x", y="y", radius="radius", color="color", source=baubles_ds)
    p.axis.visible = False
    p.grid.grid_line_color = None
    show(p);


def update_tree(levels=6, slope = 0.4, stem_width=0.2, baubles_n=20):
    patch_x, patch_y, patch_colors, baubles_x, \
    baubles_y, baubles_radius, baubles_color = compute_tree(levels, slope, stem_width, baubles_n)
    
    branches_ds.data['patch_x'] = patch_x
    branches_ds.data['patch_y'] = patch_y
    branches_ds.data['color'] = patch_colors
    
    baubles_ds.data['x'] = baubles_x
    baubles_ds.data['y'] = baubles_y
    baubles_ds.data['color'] = baubles_color
    baubles_ds.data['radius'] = baubles_radius
    replace_bokeh_data_source(branches_ds)
    replace_bokeh_data_source(baubles_ds)

In [6]:
plot_tree()



In [7]:
@interact(levels=[1,8,1], slope = [0.2,1,0.05], stem_width=[0.1,0.5,0.01], baubles_n=[5,30,1])
def plot(levels=6, slope = 0.4, stem_width=0.2, baubles_n=20):
    update_tree(levels,slope,stem_width,baubles_n)

Merry Christmas and A Happy New Year!