Heatmap color update from js_on_change ? Should something be specified in LinearColorMapper?

I’ve built a heatmap and it’s all working nicely except the colors aren’t updating from a select widget’s js_on_change as I expect it to. Everything renders, looks great, but nothing changes with the widget. Maybe there’s something I should be specifying in LinearColorMapper? Could someone please help me understand what I might be doing wrong?

Here’s a simplified example that replicates the issue:

from pandas import *
from bokeh.io import show
from bokeh.models import LinearColorMapper, ColumnDataSource, Select, CustomJS, CDSView
from bokeh.layouts import column, layout
from bokeh.plotting import figure
from bokeh.palettes import Viridis256

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                , 'dimension_1': [1, 37, 44, 13, 41, 51, 18, 14]
                , 'dimension_2': [10, 3, 44, 53, 20, 9, 18, 14]
                , 'dimension_3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

df['active_dimension'] = df['dimension_1']

source = ColumnDataSource(data=df)
view =CDSView(source=source)

values_select = Select(title="Values:", options=["dimension_1", "dimension_2", 'dimension_3'])
values_select.js_on_change('value', CustomJS(args=dict(source=source, values_select=values_select), code="""
  source.df['active_dimension'] = source.df[values_select.value]
  source.change.emit();
  p.reset.emit()
  """))
color_mapper = LinearColorMapper(palette=Viridis256, low=df.active_dimension.min(),
                                 high=df.active_dimension.max())
p = figure(x_range=periods, y_range=attributes)
p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=source,
       view=view,
       fill_color={'field': 'active_dimension', 'transform': color_mapper})

l = layout(column([values_select, p]))
show(l)

If you change the dimension and open the JavaScript console, you will see there a very clear error indicating what’s wrong with your code.

With that being said, there’s a better way to do it - without shuffling the data around. You can just change the field, transform.low, and transform.high:

from pandas import *

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, Select, CustomJS
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   , 'dimension_1': [1, 37, 44, 13, 41, 51, 18, 14]
                   , 'dimension_2': [10, 3, 44, 53, 20, 9, 18, 14]
                   , 'dimension_3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = 'dimension_1'
values_select = Select(title="Values:", options=["dimension_1", "dimension_2", 'dimension_3'], value=active)
color_mapper = LinearColorMapper(palette=Viridis256, low=df[active].min(), high=df[active].max())
p = figure(x_range=periods, y_range=attributes)
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  fill_color={'field': 'dimension_1', 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p), code="""\
    const active = cb_obj.value;
    const data = renderer.data_source.data[active];
    const {transform} = renderer.glyph.fill_color;
    transform.low = Math.min(...data);
    transform.high = Math.max(...data);
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))
1 Like

My man!! Works beautifully. I’ve adapted your code and I think I know what’s going on. It seems the main issue is once again that I don’t know Javascript lol. As I don’t want the scale of the colors to update I’ve dropped transform.low = Math.min(...data); and transform.high = Math.max(...data);.

This leads me to my next issue: instead of using a select, I’m trying to control these different dimensions with a slider, dropping the dimension_ prefix, to be left with df columns of numbers which would be strings… and sliders don’t seem to handle strings. I am predictably getting ValueError: expected a value of type Real, got 1 of type str and I can’t think of how to get around this. Any suggestions?

Thanks again for all your help, sincerely appreciated.

Glad you were able to make progress. But I can’t suggest anything without seeing the code.

Ah, sure. Changes are that I’ve renamed the df columns to ‘1’, ‘2’, ‘3’ and replaced select widget with a slider. I think the slider value is a float and the name of the df columns are strings.

I don’t particularly want to turn df column names into numbers but I thought that might be the easiest option to control dimensions with a slider. Would be happy to learn of alternatives!

from pandas import *
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, CustomJS, Slider
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   **, '1':** [1, 37, 44, 13, 41, 51, 18, 14]
                   **, '2':** [10, 3, 44, 53, 20, 9, 18, 14]
                   **, '3':** [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = **'1'**
**values_select = Slider(title="Values:", start=1, end=3, step=1, value=active)**
color_mapper = LinearColorMapper(palette=Viridis256, low=df[active].min(), high=df[active].max())
p = figure(x_range=periods, y_range=attributes)
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  fill_color={'field': 'dimension_1', 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p), code="""\
    const active = cb_obj.value;
    const data = renderer.data_source.data[active];
    const {transform} = renderer.glyph.fill_color;
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))

Ah, well, just convert active to a string where the string is required, that’s all. Also, you left one dimension_1 in there.

from pandas import *

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, CustomJS, Slider
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   , '1': [1, 37, 44, 13, 41, 51, 18, 14]
                   , '2': [10, 3, 44, 53, 20, 9, 18, 14]
                   , '3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = 1
values_select = Slider(title="Values:", start=1, end=3, step=1, value=active)
color_mapper = LinearColorMapper(palette=Viridis256, low=df[str(active)].min(), high=df[str(active)].max())
p = figure(x_range=periods, y_range=attributes)
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  fill_color={'field': str(active), 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p), code="""\
    const active = '' + cb_obj.value;
    const data = renderer.data_source.data[active];
    const {transform} = renderer.glyph.fill_color;
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))
1 Like

Ah mate that’s beautiful! Always such an obvious answer in retrospect…

Only one more question: I’ve got some tooltips now and I don’t know how to get the formatting correct… I’ve in p= I’ve included tooltips=[('', '@{active}')]) and many similar variations but that doesn’t work. Would this also need to be specified in the js_on_change?

Thanks again,

Based on the documentation at Configuring plot tools — Bokeh 2.4.2 Documentation you can supply the rect glyph with a name and then just use that name as @$name. Of course, you will have to update the name in the JS callback, yes.

Hmm ok I think I can see how the $ special character works as shown in this documentation, but I can’t work out why I can’t get that to work. There’s also a name attribute I could specify in p.rect but I don’t see how that’d be relevant.

I’m after the field that’s supplied in the rect, active. Eg. for the above example for active set to 1 I’d like Y1 to show the tooltip of ‘1’.

Here’s what I’ve drafted, basically what we’ve had before… Can’t work out why the tooltips still show ‘???’

from pandas import *
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, CustomJS, Slider
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   , '1': [1, 37, 44, 13, 41, 51, 18, 14]
                   , '2': [10, 3, 44, 53, 20, 9, 18, 14]
                   , '3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = 1
values_select = Slider(title="Values:", start=1, end=3, step=1, value=active)
color_mapper = LinearColorMapper(palette=Viridis256, low=df[str(active)].min(), high=df[str(active)].max())
p = figure(x_range=periods, y_range=attributes,
           tooltips='$name: @$name')
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  fill_color={'field': str(active), 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p), code="""\
    const active = cb_obj.value;
    const data = renderer.data_source.data[active];
    const {transform} = renderer.glyph.fill_color;
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))

And I’ll also need to figure out how to update that in the JS callback… Any hints? I should really learn JS at this point :stuck_out_tongue:

Thanks again for being patient, and sorry for the stupid questions.

But that’s exactly what’s relevant. Set it to the right value and then just use $name or @$name - depending on what you want.

Ok so I was tempted to quickly reply saying I can’t figure it out but I’ve had a long and frustrating play around with it, and I think I’ve kiiinda got it to work with $name, setting it as $active, which works properly for its current value, but I’m now struggling with the JS update. We’re getting there!

The =name gives the figure something for JS to refer to, correct? So as we already have renderer.data_source.data[active], we only need to write something to update the tooltips… but I can’t get that right… any ideas?

from pandas import *
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, CustomJS, Slider
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   , '1': [1, 37, 44, 13, 41, 51, 18, 14]
                   , '2': [10, 3, 44, 53, 20, 9, 18, 14]
                   , '3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = 1
values_select = Slider(title="Values:", start=1, end=3, step=1, value=active)
color_mapper = LinearColorMapper(palette=Viridis256, low=df[str(active)].min(), high=df[str(active)].max())
p = figure(x_range=periods, y_range=attributes, name="figure",
           tooltips='@period: @$active')
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  fill_color={'field': str(active), 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p, figure=figure), code="""\
    const active = cb_obj.value;
    const data = renderer.data_source.data[active];
    figure.tooltips = '@period: @{renderer.data_source.data[active]}'
    const {transform} = renderer.glyph.fill_color;
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))

Also, thank you so for updating tooltips to work with muted lines! I’ve updated the vis I wanted this feature for and it works perfectly! I’m thrilled! I’ve just made a donation in your name as thanks

Thanks for your kind words and for the donation.

As to your code - the @$name construct is special, it must be used verbatim. You cannot substitute name with active just like that. Also, your code doesn’t work because you’re trying to serialize the figure function in the args to CustomJS.

Here’s a working version. Notice my comments inline.

from pandas import *
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import LinearColorMapper, CustomJS, Slider
from bokeh.palettes import Viridis256
from bokeh.plotting import figure

df = DataFrame({'attribute': ['Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z']
                   , 'period': [1, 2, 3, 4, 1, 2, 3, 4]
                   , '1': [1, 37, 44, 13, 41, 51, 18, 14]
                   , '2': [10, 3, 44, 53, 20, 9, 18, 14]
                   , '3': [80, 37, 22, 13, 13, 44, 18, 14]})

df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
attributes = df.attribute.unique().tolist()

active = 1
values_select = Slider(title="Values:", start=1, end=3, step=1, value=active)
color_mapper = LinearColorMapper(palette=Viridis256, low=df[str(active)].min(), high=df[str(active)].max())
p = figure(x_range=periods, y_range=attributes,
           # Notice that I use `@$name`.
           tooltips='@period: @$name')
renderer = p.rect(x="period", y="attribute", width=1, height=1, line_color=None, source=df,
                  # And this is where the name must be set (tooltips work on glyphs after all, not on figures.
                  name=str(active),
                  fill_color={'field': str(active), 'transform': color_mapper})

values_select.js_on_change('value', CustomJS(args=dict(renderer=renderer, p=p), code="""\
    // Notice I also started using `.toString()` here to avoid JS wonkyness.
    const active = cb_obj.value.toString();
    const data = renderer.data_source.data[active];
    // Don't change the tooltips - it's not necessary.
    //p.tooltips = '@period: @{renderer.data_source.data[active]}'
    // Just change the `name` property.
    renderer.name = active;
    const {transform} = renderer.glyph.fill_color;
    renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
    p.reset.emit()
"""))

show(column([values_select, p]))
1 Like