Dynamically update color_bar

Hi, I have yet another question (sorry if I’m posting these too frequent).

I am looking for a way to update color_bar when I change the data. I have cmap, h (figure) and color_bar within data_bin_change function, and in this setup hexbin plot gets updated - selecting different data, I can see that the colormap resets to min/max values of that data, however color_bar remains the same. Here is the minimum working example, using bokeh version 2.2.3.

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
from bokeh.models import Range1d, LinearAxis


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


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 = 0.05

def normalize_data(data):
    
    #Normalized data to max
    # If data is all 0, then just parse the data
    if (data.max(axis=0) != 0):
        data_normed = data / data.max(axis=0)
    else:
        data_normed = data
        
    return data_normed


def data_bin_change(attr, old, new):
    p.xaxis.axis_label = p.yaxis.axis_label = new
    
    
    
    x_normed = normalize_data(data[new])
    y_normed = normalize_data(data[new])

    
    bins = hexbin(x_normed, y_normed, hex_size)

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


    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)
    
    
    color_bar = ColorBar(title="Counts", 
                color_mapper=cmap['transform'],
                location=(0, 0),
                ticker=LogTicker(),
                label_standoff=12,
                )
    
    return cmap, h, color_bar

key_property_x.on_change("value", data_bin_change)

data_bin_change(None, None, key_property_x.value)

cmap, h, color_bar = data_bin_change(None, None, key_property_x.value)

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

p.add_layout(color_bar, 'right')



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

I tried with placing cmap, h, and color_bar to different positions, and couldn’t achieve the goal.
If I place Figure h outside data_bin_change, the colormap does not get any updates - it just uses min/max from all data.

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

What I am trying to achieve is that with the new data, hexbin colormap updates to min/max value of that data (min is always 1), and the colorbar also updates to show new min/max values.

sorry if I’m posting these too frequent

No need to be sorry, that’s what this forum is for.

You’re creating new Bokeh models in your on_change callback - don’t do that. Try to avoid creating new models unless you’re absolutely certain that that’s exactly what you need.
Instead, just change the attributes of the existing models.

Note that even that may not work in all cases because some wiring of BokehJS models might be missing - in that case, that’s a bug that should be properly reported and eventually fixed.

Hi, thank you for suggestion. I made two changes and it’s working, however, I am not sure why it’s working. For example, how it gets maximum value for the colormap? Is this a good setup, or accidental bug that does what I want? :slight_smile:

Here is what I did:

  1. I placed the cmap, h and color_bar outside data_bin_change function
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)


color_bar = ColorBar(title="Counts", 
            color_mapper=cmap['transform'],
            location=(0, 0),
            ticker=LogTicker(),
            label_standoff=12,
            )
  1. I added col_map where I call data_bin_change and assigned it into cmap instead maximum value.
col_map = data_bin_change(None, None, key_property_x.value)
cmap = log_cmap('counts', 'Cividis256', 1, col_map)

Here is the full updated code.

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
from bokeh.models import Range1d, LinearAxis


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


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 = 0.05

def normalize_data(data):
    
    #Normalized data to max
    # If data is all 0, then just parse the data
    if (data.max(axis=0) != 0):
        data_normed = data / data.max(axis=0)
    else:
        data_normed = data
        
    return data_normed


def data_bin_change(attr, old, new):
    p.xaxis.axis_label = p.yaxis.axis_label = new
    
    
    
    x_normed = normalize_data(data[new])
    y_normed = normalize_data(data[new])

    
    bins = hexbin(x_normed, y_normed, 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.
col_map = data_bin_change(None, None, key_property_x.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)


color_bar = ColorBar(title="Counts", 
            color_mapper=cmap['transform'],
            location=(0, 0),
            ticker=LogTicker(),
            label_standoff=12,
            )


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

p.add_layout(color_bar, 'right')



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

ps. I really enjoy being on bokeh discourse, there are so many good discussions and problem solves which I found and learned from :slight_smile:

how it gets maximum value for the colormap?

From the color_mapper argument.

Is this a good setup, or accidental bug that does what I want?

At a glance, LGTM!

I really enjoy being on bokeh discourse, there are so many good discussions and problem solves which I found and learned from :slight_smile:

That’s good to hear, thanks for the feedback. :slight_smile:

1 Like