Bokeh options for user tuning of a large-dimensional multivariate algorithm


I am developing an app for a multiclass classification algorithm that can have approximately 30-40 classes in general. Some analysis methods give a user control over the contribution of each class to the solution via importance weights.

A straightforward mechanism to this is to have linear sliders, one for each class. But this seems potentially cumbersome from an interaction standpoint, and as such I am exploring other ways of intuitively providing controls.

My current thinking is a type of radial equalizer. See attached screenshot, which was generated using bokeh’s AnnularWedge glyph. I used a TapTool with Shift+Click mouse inputs to manually select points for illustration purposes.

Are there more appropriate tools that I am missing that might work for this general idea? NB: I initially tried the PolyDrawTool hoping to use it to drag and resize annular wedges in each radial direction, but it doesn’t support that glyph.

For those interested, the following simple (albeit hardcoded and messy) example illustrates the interactive part of the functionality I was able to achieve, which will suit my purposes when integrated into the larger app.

The main points: (1) an on_change callback registered to the source.selected indices is used to validate and enforce constraints such that each radial slice has exactly one selected wedge which will map to a numerical weight or equalizer level in a generic case; and (2) the tags attribute is set and unset to prevent 2X recursion into the callback as the callback is listening for changes to the indices and is also manipulating those same indices.

Representative Screenshot

Server Code

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd

from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, TapTool

# Radial grid
num_r = 10
num_th = 36

# Inner/outer radii for annular wedges
R0 = 0.1
R1 = 1.0

dr = (R1 - R0)/num_r
dth = 2.0*np.pi/num_th

_r0 =  np.linspace(R0,R1-dr,num_r)
_th = np.linspace(0.0,2.0*np.pi-dth,num_th)

r0,th = np.meshgrid(_r0, _th)

r0 = r0.flatten()
th = th.flatten()

data = np.column_stack((r0,r0+dr,th-0.5*dth,th-1.5*dth))

df = pd.DataFrame(data=data, columns=('r0','r1','th0','th1'))

source = ColumnDataSource(data=df)
sel0 = [(c*num_r + 3) for c in range(num_th)]
source.selected.indices = sel0

def eq_cb(attr, old, new):
    if source.tags:
        source.tags = []

    source.tags = ['cb']

    print("attr {:} old {:} new {:}".format(attr,old,new))

    c = new[0] // num_r
    sel0[c] = new[0]
    source.selected.indices = sel0

source.selected.on_change('indices', eq_cb)

p = figure(x_range=(-1.333,1.333), y_range=(-1.333,1.333),
           plot_width=500, plot_height=500,

eq = p.annular_wedge(x=0.0, y=0.0, 
                     inner_radius="r0", outer_radius="r1",
                     start_angle="th0", end_angle="th1",

_taptool = TapTool(renderers=[eq])
p.toolbar.active_tap = _taptool