In [1]:
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
from math import ceil

from bokeh.models import ColumnDataSource
from bokeh.io import output_file
from bokeh import events
from bokeh.plotting import gridplot,figure, show
from bokeh.layouts import widgetbox, layout
from bokeh.models.widgets import Slider, Button
from bokeh.models.callbacks import CustomJS

output_file("contour_over_time.html")

In [2]:
def reduce_evenly(sequence, num):
    """Get evenly spaced subset from given sequence.
    
    """
    
    length = float(len(sequence))
    
    if num > length:
        num = int(length)
        
    return [sequence[int(ceil(i * length / num))] for i in range(num)]


def get_contour_data(X, Y, Z, cmap, levels, data_resolution):
    """Computes countour patches from given X, Y, Z, colormap,
    levels and data resolution.
    
    https://stackoverflow.com/questions/33533047/how-to-make-a-contour-plot-in-python-using-bokeh-or-other-libs
    
    """
    
    cs = plt.contour(X, Y, Z, levels, cmap=cmap)
    xs = []
    ys = []
    col = []

    isolevelid = 0
    for isolevel in cs.collections:
        isocol = isolevel.get_color()[0]
        thecol = 3 * [None]
        theiso = str(cs.get_array()[isolevelid])
        isolevelid += 1
        for i in range(3):
            thecol[i] = int(255 * isocol[i])
        thecol = '#%02x%02x%02x' % (thecol[0], thecol[1], thecol[2])

        for path in isolevel.get_paths():
            v = path.vertices
            x = v[:, 0]
            y = v[:, 1]
            xs.append(reduce_evenly(x.tolist(), data_resolution))
            ys.append(reduce_evenly(y.tolist(), data_resolution))
            col.append(thecol)

    return xs, ys, col


def compute_kde(x, y, x_bounds, y_bounds, resolution=100j):
    """Computes KDE values for xx, yy and zz.
    
    """
    
    xx, yy = np.mgrid[x_bounds[0]:x_bounds[1]:resolution, 
                      y_bounds[0]:y_bounds[1]:resolution]
    
    positions = np.vstack([xx.ravel(), yy.ravel()])
    values = np.vstack([x, y])
    kernel = st.gaussian_kde(values)
    zz = np.reshape(kernel(positions).T, xx.shape)

    return xx,yy, zz

def create_contour_source(x, y, x_bounds, y_bounds, cmap, grid_resolution=50j, 
                         levels=5, data_resolution=50):
    """Create contour shaped ColumnDataSource for given x and y values.
    
    """
    
    ds = {}
    
    for idx in range(x.shape[1]):
        xx, yy, zz = compute_kde(x[:, idx], y[:, idx], x_bounds, y_bounds, grid_resolution)
        xs, ys, col = get_contour_data(xx, yy, zz, cmap, levels, data_resolution)
        ds["xs{}".format(idx)] = xs
        ds["ys{}".format(idx)] = ys
        ds["col{}".format(idx)] = col
    
    return ColumnDataSource(data=ds)


def generate_data(cnt, center=(0, 0)):
    """Generate uniformly distributed dummy data for visualization."""
    
    x, y = [], []
    
    for _ in range(cnt):
        data = np.random.multivariate_normal(center, [[0.8, 0.05], [0.05, 0.7]], 100)
        x.append(data[:, 0])
        y.append(data[:, 1])

    xv = np.vstack(x)
    yv = np.vstack(y)
    
    return xv.T, yv.T

In [4]:
bounds = (-3, 3)

xv, yv = generate_data(10)
b_sources = create_contour_source(xv, yv, bounds, bounds, "Blues")
b_source = ColumnDataSource(data=dict(xs=b_sources.data["xs0"], 
                                      ys=b_sources.data["ys0"], 
                                      col=b_sources.data["col0"]))

xv, yv = generate_data(10, (-1, -1))
r_sources = create_contour_source(xv, yv, bounds, bounds, "Reds")
r_source = ColumnDataSource(data=dict(xs=r_sources.data["xs0"], 
                                      ys=r_sources.data["ys0"], 
                                      col=r_sources.data["col0"]))


/linux-home/python/envs/ipython/lib/python3.5/site-packages/bokeh/models/sources.py:89: BokehUserWarning: ColumnDataSource's columns must be of the same length
  lambda: warnings.warn("ColumnDataSource's columns must be of the same length", BokehUserWarning))
/linux-home/python/envs/ipython/lib/python3.5/site-packages/bokeh/models/sources.py:89: BokehUserWarning: ColumnDataSource's columns must be of the same length
  lambda: warnings.warn("ColumnDataSource's columns must be of the same length", BokehUserWarning))

In [9]:
cb_slider = CustomJS(args=dict(b_source=b_source, 
                              b_sources=b_sources,
                              r_source=r_source,
                              r_sources=r_sources), code="""
var idx = cb_obj.value - 1;
b_source.data["xs"] = b_sources.data["xs" + idx.toString()];
b_source.data["ys"] = b_sources.data["ys" + idx.toString()];
b_source.data["col"] = b_sources.data["col" + idx.toString()];
b_source.trigger('change');

r_source.data["xs"] = r_sources.data["xs" + idx.toString()];
r_source.data["ys"] = r_sources.data["ys" + idx.toString()];
r_source.data["col"] = r_sources.data["col" + idx.toString()];
r_source.trigger('change');
""")

slider = Slider(start=1, end=10, value=1, step=1, title="Time", callback=cb_slider)

cb_button = CustomJS(args=dict(b_source=b_source, 
                               b_sources=b_sources,
                               r_source=r_source,
                               r_sources=r_sources,
                               slider=slider), code="""
var idx = slider.value
function update() {
        b_source.data["xs"] = b_sources.data["xs" + idx.toString()];
        b_source.data["ys"] = b_sources.data["ys" + idx.toString()];
        b_source.data["col"] = b_sources.data["col" + idx.toString()];
        b_source.trigger('change');

        r_source.data["xs"] = r_sources.data["xs" + idx.toString()];
        r_source.data["ys"] = r_sources.data["ys" + idx.toString()];
        r_source.data["col"] = r_sources.data["col" + idx.toString()];
        r_source.trigger('change');
        idx = idx + 1
        slider.value =idx
        
        if (idx < 10) {
            setTimeout(update, 500)
        }
}

update()
""")

button = Button(label="Iterate over Time")
button.js_on_event(events.ButtonClick, cb_button)

In [10]:
plot = figure(plot_width=800,plot_height=500, x_range=(-3, 3), y_range=(-3, 3))
plot.patches(xs='xs', ys='ys', line_color='col', fill_color="col", fill_alpha=0.2, source=b_source)
plot.patches(xs='xs', ys='ys', line_color='col', fill_color="col", fill_alpha=0.2, source=r_source)
show(layout([[slider, button], [plot]]))

In [ ]: