Bokeh plot not plotting negative values with circle during python callback

Have tried to replicate the issue with the below example. The chart updates based on selection, and multiple selection allow for work to be done before rendering the plot. The positive values are plotting as expected, and if the result of the multiple selection is positive, the chart is correctly rendered. However if the result is negative, the chart does not render correctly. Below run with bokeh serve.

import bokeh
import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.plotting import figure, show
from bokeh.layouts import column, row, layout
from bokeh.io import curdoc
import datetime
import numpy as np
import pytz

today = datetime.datetime.now(pytz.timezone(‘Europe/London’))
df = pd.DataFrame([{‘a’:2},{‘a’:-3},{‘a’:5}])
df[‘Indexer’] = np.arange(len(df))
today_list = [today]

ds = ColumnDataSource(df)
columns = [TableColumn(field =‘a’, title = ‘a’, width = 50)]
dt = DataTable(source = ds, columns = columns, height = 100, width = 100, fit_columns = True)

plot = figure(x_axis_type=‘datetime’,plot_width=400, plot_height=400)

dot_ds = ColumnDataSource ({‘x’: pd.to_datetime(today_list), ‘y’: [0]})
dot = plot.circle(x=‘x’, y=‘y’, source = dot_ds, alpha = 0.5, size=5, color=“navy”)
dot.visible = False

info_source = ColumnDataSource(dict(indexer_1 = , indexer_2 = , indexer_3= ))

#Below JS stores click indexes in a column data source which is then retrievable in python callback for future work:

code = ‘’’

///Sorting function to required to order selections:

const median = arr => {
  const mid = Math.floor(arr.length / 2),
    nums = [...arr].sort((a, b) => a - b);
  return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
};

        
 var inds = cb_obj.indices
 var data = source.data

 if (inds.length == 1){
       
        var selected_index = cb_obj.indices[0];
        var indexer = data['Indexer'][selected_index];
        info_source.data = {indexer_1:[indexer],indexer_2:[''], indexer_3:['']}
        
        }

 if (inds.length == 2){
       
        var selected_index_1 = cb_obj.indices[0]
        var selected_index_2 = cb_obj.indices[1];

        var selected_index_b = Math.min(selected_index_2, selected_index_1)
        var selected_index_a = Math.max(selected_index_2, selected_index_1)
        
        var indexer_1 = data['Indexer'][selected_index_a]
        var indexer_2 = data['Indexer'][selected_index_b];
        
        info_source.data = {indexer_1:[indexer_1],indexer_2:[indexer_2], indexer_3:['']}
        
       }
        
        
if (inds.length == 3){

        var selected_index_1 = cb_obj.indices[0]
        var selected_index_2 = cb_obj.indices[1]
        var selected_index_3 = cb_obj.indices[2]
        ;

        var selected_index_c = Math.min(selected_index_3, selected_index_2, selected_index_1)
        var selected_index_b = median([selected_index_3, selected_index_2, selected_index_1])
        var selected_index_a = Math.max(selected_index_3, selected_index_2, selected_index_1)

        console.log(selected_index_a)
        console.log(selected_index_b)
        console.log(selected_index_c)


        var indexer_1 = data['Indexer'][selected_index_b]
        var indexer_2 = data['Indexer'][selected_index_a]
        var indexer_3 = data['Indexer'][selected_index_c]
        ;
        
        info_source.data = {indexer_1:[indexer_1],indexer_2:[indexer_2],indexer_3:[indexer_3]}
        
        }
        
        '''

callback = CustomJS(args = {‘source’ : ds, ‘info_source’: info_source}, code = code)
ds.selected.js_on_change(‘indices’, callback)

def py_callback(attr, old, new):

indexer_1 = (info_source.data['indexer_1'][0])
indexer_2 = (info_source.data['indexer_2'][0])
indexer_3 = (info_source.data['indexer_3'][0])

#check for three clicks

if indexer_3 != '':

    dot_value = df['a'][indexer_3] - df['a'][indexer_2] - df['a'][indexer_1]

elif indexer_2 != '':

    dot_value = df['a'][indexer_2] - df['a'][indexer_1]

else:

    dot_value = df['a'][indexer_1]

dot_ds.data = {'x': [pd.to_datetime(today_list)] ,'y': [dot_value]}
dot.visible = True

ds.selected.on_change(‘indices’, py_callback)

col = column(plot, dt)
curdoc().add_root(col)

Hi @jbrown113 please first edit your post to use code formatting so that the code is intelligible (either with the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks)

Hi Bryan, apologies I have tried to edit my post however I get a 403 - Forbidden error when I try to do so. I also tried to delete my post and make a new topic, however unclear how I delete what I have already posted. In case it helps, I will copy the code in this reply (although far from ideal, don’t see what else I can do)

Have tried to replicate the issue with the below example. The chart updates based on selection, and multiple selection allow for work to be done before rendering the plot. The positive values are plotting as expected, and if the result of the multiple selection is positive, the chart is correctly rendered. However if the result is negative, the chart does not render correctly. Below run with bokeh serve.

import bokeh
import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.plotting import figure, show
from bokeh.layouts import column, row, layout
from bokeh.io import curdoc
import datetime
import numpy as np
import pytz

today = datetime.datetime.now(pytz.timezone('Europe/London'))  
df = pd.DataFrame([{'a':2},{'a':-3},{'a':5}])
df['Indexer'] = np.arange(len(df))
today_list = [today]

ds = ColumnDataSource(df)
columns = [TableColumn(field ='a', title = 'a', width = 50)]
dt = DataTable(source = ds, columns = columns, height = 100, width = 100, fit_columns = True)

plot = figure(x_axis_type='datetime',plot_width=400, plot_height=400)

dot_ds =  ColumnDataSource ({'x': pd.to_datetime(today_list), 'y': [0]})
dot = plot.circle(x='x', y='y', source = dot_ds, alpha = 0.5, size=5, color="navy")
dot.visible = False

info_source = ColumnDataSource(dict(indexer_1 = [], indexer_2 = [], indexer_3=[] )) 

#Below JS stores click indexes in a column data source which is then retrievable in python callback for future work:

code = '''


    ///Sorting function to required to order selections:
    
    const median = arr => {
      const mid = Math.floor(arr.length / 2),
        nums = [...arr].sort((a, b) => a - b);
      return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
    };

            
     var inds = cb_obj.indices
     var data = source.data
    
     if (inds.length == 1){
           
            var selected_index = cb_obj.indices[0];
            var indexer = data['Indexer'][selected_index];
            info_source.data = {indexer_1:[indexer],indexer_2:[''], indexer_3:['']}
            
            }
    
     if (inds.length == 2){
           
            var selected_index_1 = cb_obj.indices[0]
            var selected_index_2 = cb_obj.indices[1];

            var selected_index_b = Math.min(selected_index_2, selected_index_1)
            var selected_index_a = Math.max(selected_index_2, selected_index_1)
            
            var indexer_1 = data['Indexer'][selected_index_a]
            var indexer_2 = data['Indexer'][selected_index_b];
            
            info_source.data = {indexer_1:[indexer_1],indexer_2:[indexer_2], indexer_3:['']}
            
           }
            
            
    if (inds.length == 3){

            var selected_index_1 = cb_obj.indices[0]
            var selected_index_2 = cb_obj.indices[1]
            var selected_index_3 = cb_obj.indices[2]
            ;

            var selected_index_c = Math.min(selected_index_3, selected_index_2, selected_index_1)
            var selected_index_b = median([selected_index_3, selected_index_2, selected_index_1])
            var selected_index_a = Math.max(selected_index_3, selected_index_2, selected_index_1)

            console.log(selected_index_a)
            console.log(selected_index_b)
            console.log(selected_index_c)


            var indexer_1 = data['Indexer'][selected_index_b]
            var indexer_2 = data['Indexer'][selected_index_a]
            var indexer_3 = data['Indexer'][selected_index_c]
            ;
            
            info_source.data = {indexer_1:[indexer_1],indexer_2:[indexer_2],indexer_3:[indexer_3]}
            
            }
            
            '''


callback = CustomJS(args = {'source' : ds, 'info_source': info_source}, code = code)
ds.selected.js_on_change('indices', callback) 

def py_callback(attr, old, new):

    indexer_1 = (info_source.data['indexer_1'][0])
    indexer_2 = (info_source.data['indexer_2'][0])
    indexer_3 = (info_source.data['indexer_3'][0])

    #check for three clicks

    if indexer_3 != '':

        dot_value = df['a'][indexer_3] - df['a'][indexer_2] - df['a'][indexer_1]

    elif indexer_2 != '':

        dot_value = df['a'][indexer_2] - df['a'][indexer_1]

    else:

        dot_value = df['a'][indexer_1]

    dot_ds.data = {'x': [pd.to_datetime(today_list)] ,'y': [dot_value]}
    dot.visible = True

    
ds.selected.on_change('indices', py_callback)
    
col = column(plot, dt)
curdoc().add_root(col)

@jbrown113 First a couple of unrelated notes:

  • plot_width and plot_height are going away in Bokeh 3.0, to future-proof your code you should just use width and height like every other layout-able.

  • There is no guarantee about relative ordering of Python vs JS callbacks on the same property, so I think your Python callback would be much safer as

    info_source.on_change('data', py_callback)
    

    which will clearly only trigger after info_source is guaranteed to be updated.

That said, I don’t have an explanation for the behavior. Bokeh can clearly plot negative values on auto-ranged plots, there are dozens of such examples in the docs and repo. But, something about your code is causing the auto-range start to be computed as NaN, which is why nothing shows up. I will say, your code seems somewhat unusual, and is structured in a way that I have not really ever encountered. I would suggest making a GitHub Issue but before doing that, work to make the MRE more minimal. As a goal, shoot for an MRE that is under 50 lines. It’s entirely possible that focusing the MRE will itself lead to the solution or a better understanding of the problem.

1 Like

Thanks for coming back so quickly. I will try and reduce the MRE, but perhaps also as a quick fix in the mean time setting the range via the python callback could potentially help the issue I am facing at the moment. Appreciate the comments re future-proofing and the callback.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.