Dynamical Axes Selection - multiple Select widgets

Hi,

this glitch has been bothering me for a while. I have 3 Select options: Group contains a list of valid properties for Option X and Option Y - which are then translated into the plot, thus plot updates after every Select option.

The issue: When selecting Option X, the ‘Option Y’ is reset to [0] item - and I would like it to stay as it was. You can see that if you select first ‘Option Y’ to e.g. z, and then you want Option X to be y, the plot is updated and the Y will be reset to x and not keeping its value.

Expected:
On Group select: Option X and Option Y gets [0] items from the active group (group here has always x, y, z, but it is generally different - and this part works with this setup). So it will plot x vs x.
Selecting X or Y does not reset each other to [0].

I believe the error is within circular Select options that I have here, however, if I place update_group in group; and update_x in key_property_x, and update_y in key_property_y – Nothing works.

group.on_change("value", update_x)
key_property_x.on_change("value", update_y)
key_property_y.on_change("value", update_group)

Here is the minimum executive code (I removed additional colorbar lines for clarity):

import numpy as np

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select
from bokeh.plotting import figure
from bokeh.transform import log_cmap
from bokeh.util.hex import hexbin


data = {'x': np.random.randint(10, size=1000),
        'y': np.random.randint(20, size=500),
        'z': np.random.randint(50, size=100)}


options = sorted(data.keys())

def update_group(attr, old, new):
    
    key_property_x.options = options
    key_property_y.options = options

    return old, new
    
def update_x(attr, old, new):  

    key_property_x.options = options
    key_property_x.value = options[0]

    return old, new
    
def update_y(attr, old, new):  
    key_property_y.options = options
    key_property_y.value = options[0]

group = Select(title="Group", value=options[0], options=options)
key_property_x = Select(title="Option X:", value=options[0], options=options)
key_property_y = Select(title="Option Y:", value=options[0], options=options)


group.on_change("value", update_x)
key_property_x.on_change("value", update_y)
key_property_y.on_change("value", update_group)

def update_plot():
    return dict(x=data[key_property_x.value],
                y=data[key_property_x.value])


p = figure(tools='pan,box_zoom,box_select,reset,wheel_zoom, undo,redo,save',
           match_aspect=True, background_fill_color='white', toolbar_location="above")
p.grid.visible = False

source_bin = ColumnDataSource(data=dict(r=[], q=[], counts=[]))
hex_size = 1


def data_bin_change(attr, old, new):
    p.xaxis.axis_label = key_property_x.value
    p.yaxis.axis_label = key_property_y.value
    
    x = data[new]
    y = data[new]

    bins = hexbin(x, y, hex_size)
    source_bin.data = dict(r=bins.r, q=bins.q, counts=bins.counts)

key_property_x.on_change("value", data_bin_change)
key_property_y.on_change("value", data_bin_change)

col_map = data_bin_change(None, None, group.value)
cmap = log_cmap('counts', 'Cividis256', 1, col_map)
h = p.hex_tile(q="q", r="r", size=hex_size, line_color=None,
           source=source_bin, fill_color=cmap)


curdoc().add_root(row(column(group, key_property_x, key_property_y), p))

I could really use help how to set up update_x and update_y so that one doesn’t reset the other (in this case OptionX resets OptionY). I tried various combinations of my current on_change functions, and none of them worked.

@rdzudzar I am afraid your description is fairly confusing to me. Is the code below not what you are after?

def update_group(attr, old, new):

    # set new x-options and select x-value, based on group
    key_property_x.options = options
    key_property_x.value = options[0]

    # set new y-options and select y-value, based on group
    key_property_y.options = options
    key_property_y.value = options[0]

# update the plot from current x- and y- select values
def update_plot(attr, old, new):
    return dict(x=data[key_property_x.value],
                y=data[key_property_x.value])

# group callback only affects x- and y- selects
group.on_change("value", update_group)

# both select callbacks update the plot (whether from a user setting
# the value directly, or from group callback changing it)
key_property_x.on_change("value", update_plot)
key_property_y.on_change("value", update_plot)

FYI there is never any reason I can think of for callbacks to return anything. Bokeh invokes the callback code and does not expect or do anything with any return value

Hi @Bryan,

I apologise, my minimum example had an error - the data that I am getting was not in the correct format, originally I am using nested .h5 file, and this new data from nested dictionary corresponds to that structure.

With your suggestions, the Option X and Option Y behave like they should - one does not reset the other. However, now when I change Groups from A to B - the Options X and Y don’t update and I get an error:

  File ..." line 86, in data_bin_change
    x = data[group.value][key_property_x.value]
KeyError: 'A'
c:\users\rob\anaconda3\lib\site-packages\bokeh\server\protocol_handler.py:94: RuntimeWarning: coroutine 'WSHandler.send_message' was never awaited
  work = connection.error(message, repr(e))

Expected outcome on Group change: shows Group B, Otions X/Y: a, b, c and plot updates to e.g. new [0], [0] properties.

Here is the updated example, having new data structure

import numpy as np

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select
from bokeh.plotting import figure
from bokeh.transform import log_cmap
from bokeh.util.hex import hexbin

data = {'A':{
        
        'x': np.random.randint(10, size=500),
        'y': np.random.randint(20, size=500),
        'z': np.random.randint(50, size=500)
            },
        'B':{
        'a': np.random.randint(10, size=100),
        'b': np.random.randint(20, size=100),
        'c': np.random.randint(50, size=100)
            }
        }

groups = ['A', 'B']

dictionary = {'A': ['x', 'y', 'z'],
              'B': ['a', 'b', 'c']}

options = sorted(data.keys())


def update_group(attr, old, new):
    
    key_property_x.options = options
    key_property_x.value = options[0]
    
    key_property_y.options = options
    key_property_y.value = options[0]
        
group = Select(title="Groups", value=groups[0], options=groups)

key_property_x = Select(title="Option X:", value=dictionary[group.value][0], options=dictionary[group.value])
key_property_y = Select(title="Option Y:", value=dictionary[group.value][0], options=dictionary[group.value])

group.on_change("value", update_group)


def update_plot():
    return dict(x=data[group.value][key_property_x.value],
                y=data[group.value][key_property_y.value])


p = figure(tools='pan,box_zoom,box_select,reset,wheel_zoom, undo,redo,save',
           match_aspect=True, background_fill_color='white', toolbar_location="above")
p.grid.visible = False

source_bin = ColumnDataSource(data=dict(r=[], q=[], counts=[]))
hex_size = 1


def data_bin_change(attr, old, new):
    p.xaxis.axis_label = key_property_x.value
    p.yaxis.axis_label = key_property_y.value
    
    
    x = data[group.value][key_property_x.value]
    y = data[group.value][key_property_y.value]

    bins = hexbin(x, y, hex_size)

    source_bin.data = dict(r=bins.r, q=bins.q, counts=bins.counts)


key_property_x.on_change("value", data_bin_change)
key_property_y.on_change("value", data_bin_change)

col_map = data_bin_change(None, None, group.value)

cmap = log_cmap('counts', 'Cividis256', 1, col_map)

h = p.hex_tile(q="q", r="r", size=hex_size, line_color=None,
           source=source_bin, fill_color=cmap)


curdoc().add_root(row(column(group, key_property_x, key_property_y), p))

In the group callback, you are setting key_property_x.options to ["A", "B"] instead of the nested sub-options based on the group. You will need to account for that. You will also need to use hold and unhold to collect events for one update at the end, instead of triggering intermediate events while the overall state is not consistent with the newly set options:

options = {'A': ['x', 'y', 'z'],
           'B': ['a', 'b', 'c']}

def update_group(attr, old, new):
    curdoc().hold("combine")
    key_property_x.options = options[group.value]
    key_property_x.value = options[group.value][0]

    key_property_y.options = options[group.value]
    key_property_y.value = options[group.value][0]
    curdoc().unhold()

Note you will also need to rename dictionary to options everywhere to use the code above as-is.

BTW it’s a bit unusual to register multiple callbacks for the same property. Obviously we do permit this but it seems that your update_plot and data_bin_change could just be combined (or one could call the other as a regular function)

Thank you!
I wasn’t aware of the hold and unhold.

    curdoc().hold("combine")
    curdoc().unhold()

The solution works with the shown example as well as in the original code.
And I agree, in this example update_plot is not necessary.

1 Like