Bokeh hexbin and widget Select

Hi,
I’m trying to connect a drop down menu using Select with a hexbin plot, however whatever I try I can not update my source.data['x'] and plot doesn’t update when I change item in the drop down menu. I tried using customJS and on_change options on bokeh server and can not make it work. (I made scatterplots and it’s all fine, the density plots are those that I want and have issue with). Can anyone help me out with this? At this point I’m wondering is it possible what I aim to do?

Here is minimum executable example below (version for bokeh server, bokeh version 2.1.1):

import numpy as np
from bokeh.layouts import column, row, gridplot
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.transform import log_cmap
from bokeh.util.hex import hexbin
from bokeh.io import curdoc
from bokeh.models import (ColorBar, LogTicker, HoverTool, 
                          ColumnDataSource, CustomJS, Select)

example_data = {
    'x': np.random.randint(30, size=50),
    'y': np.random.randint(20, size=50),
    'z': np.random.randint(10, size=50)
    }

#Initiate ColumnDataSources
source = ColumnDataSource(data = dict(x = [], y = []) )


def update_x(attr, old, new):  
    key_property_x.value


key_property_x = Select(title="Option:", value=f"{list(example_data)[0]}",
                  options=list(example_data))
key_property_x.on_change("value", update_x)

#Update Data Source
def update_plot():
 
    p.xaxis.axis_label = key_property_x.value
    p.yaxis.axis_label = key_property_x.value
    
    source.data = dict(x = example_data[key_property_x.value],
                        y = example_data[key_property_x.value])   
 
    hex_size = 1
    bins = hexbin(source.data['x'], source.data['y'], hex_size)
 
    return bins, hex_size


#Defining Figure Tools
defaults = 'pan,box_zoom,box_select,reset,wheel_zoom, undo,redo,save'
TOOLS = [defaults]#,hover_userdefined]

p = figure(tools=TOOLS, match_aspect=True, background_fill_color='white', 
           toolbar_location="above")
p.grid.visible = False

bins, hex_size = update_plot()


cmap = log_cmap('counts', 'Cividis256', 1, max(bins.counts))

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



hover = HoverTool(
    tooltips=[('Counts', '@counts')],
    mode='mouse',
    point_policy='follow_mouse',
    renderers=[h],
)
p.add_tools(hover)

color_bar = ColorBar(
    color_mapper=cmap['transform'],
    location=(0, 0),
    ticker=LogTicker(),
)
p.add_layout(color_bar, 'right')


col = column(key_property_x)
layout = row(col, p)

curdoc().add_root(layout)

Notice how hexbin doesn’t accept source as a parameter - it accepts only the actual data values. It means that even if you change the source, bins will not change and p.hex_tile will not as well.

You don’t need CDS prior to the call to hexbin, but you need one after that. And that resulting source could be used in the callback. You will have to explicitly wrap bins in ColumnDataSource because bins is a data frame and not an actual source.

Yes, having source=bins in hex_tile is what is putting me off the track. I am a bit confused with the statement that I don’t need CDS prior to hexbin. Since I populate my data from the file using that CDS - maybe my example was off a bit, in the original code I am using .h5 file and populate data like below, so I am not sure how can I be without it:

filename = 'File.h5'
source = ColumnDataSource(data = dict(x = [], y = []) 

Which I populate with def update_plot():

def update_plot():
    with h5py.File(filename, 'r') as f:
        source.data = dict(x = f[group.value][key_property_x.value][:],
                           y = f[group.value][key_property_y.value][:],                           
                           )

Can you please provide me some example of what you are saying with bin wrapping?

You still need some data storage, it just doesn’t have to be an instance of ColumnDataSource since Bokeh never even sees that instance - all you get out of it is its data property which is a dict. So you can just use that dict directly, without wrapping it into ColumnDataSource.

I understand the part with the CDS and initial data (I think, but my code at the moment doesn’t work without it), however, I am still not able to properly wrap the bin in callback, I could really use example how to do that.

In the current code (below) I can populate x and y from dropdown menu, and the hexbin plot will appear, however, it will break for z because I do not have it explicitly in my data. With the CDS for data from .h5py file (as in previous code snippet) this error on z should not happen, because having over 100 columns can’t define all of them as in the example_data that is why I used the previous function for .h5py.

So I am still getting not complete output and at the moment can’t find the proper solution.

import numpy as np
from bokeh.layouts import column, row, gridplot
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.transform import log_cmap
from bokeh.util.hex import hexbin
from bokeh.io import curdoc
from bokeh.models import (ColorBar, LogTicker, HoverTool, 
                          ColumnDataSource, CustomJS, Select)

example_data = {
    'x': np.random.randint(30, size=50),
    'y': np.random.randint(20, size=50),
    'z': np.random.randint(10, size=50)
    }


def update_x(attr, old, new):  
    key_property_x.value


key_property_x = Select(title="Option:", value=f"{list(example_data)[0]}",
                  options=list(example_data))
key_property_x.on_change("value", update_x)

#Update Data Source
def update_plot():
 
    p.xaxis.axis_label = key_property_x.value
    p.yaxis.axis_label = key_property_x.value
    
    data = dict(x = example_data[key_property_x.value],
                y = example_data[key_property_x.value],
                )   
 
 
    return data


#Defining Figure Tools
defaults = 'pan,box_zoom,box_select,reset,wheel_zoom, undo,redo,save'
TOOLS = [defaults]#,hover_userdefined]

p = figure(tools=TOOLS, match_aspect=True, background_fill_color='white', 
           toolbar_location="above")
p.grid.visible = False


# BINS 
source_bin = ColumnDataSource(data = dict(r = [], q = [], counts = []) )

hex_size = 1


def data_bin_change(attr, old, new):

    data = update_plot()
    bins = hexbin(data[key_property_x.value], data[key_property_x.value], hex_size) 
    
    
    source_bin.data = dict(r = bins.r, 
                           q = bins.q,
                           counts = bins.counts
                                  )

key_property_x.on_change("value", data_bin_change)

def bin_source():
    
      
    data = update_plot()
    bins = hexbin(data[key_property_x.value], data[key_property_x.value], hex_size) 


    cmap = log_cmap('counts', 'Cividis256', 1, max(bins.counts))

    return source_bin, cmap, hex_size

source_bin, cmap, hex_size = bin_source()

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



hover = HoverTool(
    tooltips=[('Counts', '@counts')],
    mode='mouse',
    point_policy='follow_mouse',
    renderers=[h],
)
p.add_tools(hover)

color_bar = ColorBar(
    color_mapper=cmap['transform'],
    location=(0, 0),
    ticker=LogTicker(),
)
p.add_layout(color_bar, 'right')


col = column(key_property_x)
layout = row(col, p)

curdoc().add_root(layout)

This is how I’d do it:

import numpy as np

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

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

options = sorted(data.keys())
key_property_x = Select(title="Option:", value=options[0], options=options)


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 = p.yaxis.axis_label = new
    bins = hexbin(data[new], data[new], hex_size)

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


key_property_x.on_change("value", data_bin_change)

# Populate the initial data.
data_bin_change(None, None, key_property_x.value)

# I'm not sure whether the `cmap` should be reset in `data_bin_change`.
# After all, it depends on the data that's computed by `hexbin`.
cmap = log_cmap('counts', 'Cividis256', 1, max(source_bin.data['counts']))

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

p.add_tools(HoverTool(tooltips=[('Counts', '@counts')],
                      mode='mouse',
                      point_policy='follow_mouse',
                      renderers=[h]))

p.add_layout(ColorBar(color_mapper=cmap['transform'],
                      location=(0, 0),
                      ticker=LogTicker()),
             'right')

curdoc().add_root(row(column(key_property_x), p))
1 Like

Thank you very much! I was way overthinking it with double CDS, and this is neat:

Moreover, I was able to make a change so that I can read data from the .h5py file within data_bin_change function, and on the first look, all seems to work great.

Thanks again for your time and help.