Search code examples
javascriptpythonplotbokehinteractive

Bokeh and grid plot with arbitrary entries, MultiChoice & CustomJS for changing data


I am trying to represent data organized as a matrix of vector-valued timeseries. Given a vector timeseries placed at position (i,j) in the matrix, the scalar timeseries components are obtained by some parameter 'order'. The size of the matrix is arbitrary and cannot be hard-coded. I want to

  • plot each entry (timeseries) of the matrix,
  • change which component of the timeseries is represented, using Bokeh's MultiChoice callback and updating all plots simultaneously.

I managed to do what I want on an example where the callback is written explicitly for each component of my matrix, by adapting an example seen here: Filtering data source for Bokeh plot using MultiChoice and CustomJS

import pandas as pd
import numpy as np

from bokeh.plotting import figure, show, row, column
from bokeh.models import ColumnDataSource, CustomJS, MultiChoice

# Generate some data structure
tau = np.linspace(0,2*np.pi,100)
N = 2
orders = [1,2,3]
df = pd.DataFrame()
for o in orders:
    df_o = pd.DataFrame()
    for i in range(N):
        for j in range(N):
            df_o[f'{i}{j}'] = i+j + np.cos(tau+o)
    df_o['order'] = str(o)
    df_o['tau'  ] = tau
    df = df.append(df_o,ignore_index=True)

# Bokeh plot
order_dict = {}
plots = []
for i in range(N):
    plots_line_i = []
    for j in range(N):
        plot_ij = figure(width=400, height=400)
        name_ij = f'{i}{j}'
        order_dict[name_ij] = {'order':[],'label':[]}
        for order in df.order.unique():  
            source = ColumnDataSource(df[df['order']==order])
            order_glyph_ij = plot_ij.line('tau', name_ij, source=source)
            order_dict[name_ij]['order'].append(order_glyph_ij)
            order_dict[name_ij]['label'].append(order)
        plots_line_i.append(plot_ij)
    plots.append(plots_line_i)

# Set up MultiChoice widget
initial_value = [df.order[0]]
options = list(df.order.unique())
multi_choice = MultiChoice(value=initial_value, options=options, max_items=3, title='Selection:')

# Set up callback: how to automatically loop within the dict keys '00', '01', '10', '11' ?
callback = CustomJS(args=dict(order_dict=order_dict, multi_choice=multi_choice), code="""
var selected_vals = multi_choice.value;
var index_check = [];

for (var i = 0; i < order_dict['00']['order'].length; i++) {

    index_check[i]=selected_vals.indexOf(order_dict['00']['label'][i]);
        if ((index_check[i])>= 0) {
            order_dict['00']['order'][i].visible = true;
            }
        else {
            order_dict['00']['order'][i].visible = false;
        }
    }

for (var i = 0; i < order_dict['11']['order'].length; i++) {
    index_check[i]=selected_vals.indexOf(order_dict['11']['label'][i]);
        if ((index_check[i])>= 0) {
            order_dict['11']['order'][i].visible = true;
            }
        else {
            order_dict['11']['order'][i].visible = false;
        }
    }

for (var i = 0; i < order_dict['01']['order'].length; i++) {
    index_check[i]=selected_vals.indexOf(order_dict['01']['label'][i]);
        if ((index_check[i])>= 0) {
            order_dict['01']['order'][i].visible = true;
            }
        else {
            order_dict['01']['order'][i].visible = false;
        }
    }

for (var i = 0; i < order_dict['10']['order'].length; i++) {
    index_check[i]=selected_vals.indexOf(order_dict['10']['label'][i]);
        if ((index_check[i])>= 0) {
            order_dict['10']['order'][i].visible = true;
            }
        else {
            order_dict['10']['order'][i].visible = false;
        }
    }
""")
multi_choice.js_on_change('value', callback)

# Display the grid plot
rows = []
for i in range(N):
    rows.append(row(*plots[i]))
show(column(*rows,multi_choice))

This code normally shows a 2x2 matrix plot:

2x2 matrix plot

In the above code, you will see four 'for' loops in the JavaScript code (one for each entry '00', '01', '10', '11') of the matrix. Would it be possible to do that automatically by somehow going through the keys of the dictionary 'order_dict' ?

This may be a stupid question, but I don't know JavaScript so I'm having trouble debugging my code...


Solution

  • You can simplify your JS-Code to

    callback = CustomJS(args=dict(order_dict=order_dict, multi_choice=multi_choice), code="""
    var selected_vals = multi_choice.value;
    var index_check = [];
    for (var k in order_dict){
        for (var i = 0; i < order_dict[k]['order'].length; i++) {
            index_check[i]=selected_vals.indexOf(order_dict[k]['label'][i]);
            order_dict[k]['order'][i].visible = index_check[i] >= 0
        }
    }
    """)
    

    This is working exactly the same like your code.

    The solution is based on this post.

    Comment

    Your python code gives my a deprecation warning using pandas 1.4.+.

    FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.

    You can avoid this, changing

    df = df.append(df_o,ignore_index=True)
    
    df = pd.concat([df, df_o],ignore_index=True)
    

    If you don't see this warning, you should consider to update your pandas version.