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"]))
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 [ ]: