Updating X-axis range using CustomJS callback and sliders not working

I’m trying to plot a scatter plot using Bokeh, and then give the user the option to change how many points are displayed on the plot based on the time window (specified by the range slider). The time window itself is not on the axes of the graph.

I tried to update the x_range slice based on the slider values (which is the method used in the official Bokeh tutorials page), but this is not working for me. I’ve written the code inside the CustomJS function to get the slider values, use them to find the indices of the corresponding x_axis values. However, the x-axis is not changing.

My code:

sample_data = {'Time': [1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12, 12, 12, 13, 14, 14],
 'A': ['eggs',  'eggs',  'eggs',  'eggs',  'eggs',  'eggs',  'banana',  'banana',  'banana',  'banana',  
       'banana',  'milk',  'milk',  'milk',  'milk',  'milk',  'sugar',  'sugar',  'sugar',  'sugar'],
 'X': ['id1',  'id2',  'id3',  'id4',  'id5',  'id6',  'id7',  'id8',  'id9',  'id10',  'id11',  'id12',  'id13',  'id14',  'id15',
  'id16',  'id17',  'id18',  'id19',  'id20'],
 'Y': [26,  21, 49,  28,  22,  31,  33,  45,  42,  27,  11,  12,  42,  43,  36,  43,  30,  41,  16,  21]}

source = ColumnDataSource(sample_data)

TOOLTIPS= [
    ("X","@X"),
    ("Y", "@Y"),
    ("A", "@A"),
    ("Time", "@Time"),
    ]

plot = figure(x_range = source.data['X'], tooltips = TOOLTIPS,sizing_mode = 'stretch_width', tools = ('pan','xpan','ypan','hover','box_zoom','zoom_in','zoom_out','reset'))

plot.circle("X", "Y", source = source,size = 10 )
plot.title.text = 'All Values'

# Set up Slider widget
number_slider = RangeSlider(
    start=source.data['Time'][0],
    end=source.data['Time'][-1],
    step = 1,
    value=(source.data['Time'][0],source.data['Time'][-1]),
    title="Time Window",
)
  
# Set up CustomJS callback
custom_js = CustomJS(
    args={  # the args parameter is a dictionary of the variables that will be accessible in the JavaScript code
        "plot":plot,  # the first variable will be called "plot" and links to the largest_carriers_plot Python object
        "x": source.data["X"],  # links to list of X in the source ColumnDataSource
        "time": source.data["Time"] #links to list of times in the source
    },
    code="""
    start = this.value[0] //start time value
    end = this.value[1] //end time value
    
    //find index values of these corresponding timestamps
    start_idx = time.indexOf(start)
    end_idx = time.indexOf(end)
    plot.x_range = x.slice(start_idx,end_idx)  
    plot.x_range.change.emit()
    """
)

# Add callback to slider widget
number_slider.js_on_change("value", custom_js)

layout = column([number_slider, plot], sizing_mode="stretch_width")
show(layout)

Hello Animesh_Gupta, welcome to Bokeh discourse.

Don’t forget to give the imports in your code, which are necessary for the readers to test your code.
Also, it is advisable to mention your Bokeh version and your operating system.

Try using this Javascript code:

const start = this.value[0] //start time value
const end = this.value[1] //end time value
    
// find index values of these corresponding timestamps
const start_idx = time.indexOf(start)
const end_idx = time.indexOf(end)
plot.x_range.factors = x.slice(start_idx,end_idx)  
plot.x_range.change.emit()

Changed:

  • Variable declaration with const.
  • Added .factors to plot.x_range

This works for me.

You know that you can press F12 in your browser while running your app to display Javascript errors in the console? This would have given you hints about the variable declaration.

2 Likes

Thanks a lot @Icoti, its working now!
Also thanks a bunch for the tips, I’ll keep those in mind going forward!

1 Like

@Animesh_Gupta, glad to hear that this solved your problem.
Could you mark the corresponding post as solution so readers can find it easier?

1 Like

Hi @Icoti, I marked your reply as the solution, but I do have a follow up question.

I tried implementing this exact code (with the appropriate name changes) to another dataset having the same structure, but now I’m getting a “[bokeh] could not set initial ranges” error on the Javascript console whenever I move the sliders. Any clues as to why that could be?

Thanks.

Edit: Another question: the method I am using to filter that data relies on the time column being sorted. How would I achieve the same result for an unsorted Time column?

It is impossible to reliably answer your question without a (small) example dataset leading to the error. Please provide such a dataset.

I am not sure what you mean with “unsorted Time column”, and what you expect. Here too, please provide a small exemplary dataset with unsorted Time column, with a screenshot of what you get and a description of what you expect.

Hey @Icoti, sorry about that, I’ll try to explain properly:

By Unsorted Time column, I meant if the values are not in ascending order (which they were in the code I had posted in the question).
Keeping the other columns the same, if the Time column becomes:

'Time': [8, 9, 10, 11, 12, 12, 12, 13, 14, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 14]

So the overall data becomes:

{'Time': [8, 9, 10, 11, 12, 12, 12, 13, 14, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7,  14],
 'A': ['eggs',  'eggs',  'eggs',  'eggs',  'eggs',  'eggs',  'banana',  'banana',  'banana',  'banana',  
       'banana',  'milk',  'milk',  'milk',  'milk',  'milk',  'sugar',  'sugar',  'sugar',  'sugar'],
 'X': ['id1',  'id2',  'id3',  'id4',  'id5',  'id6',  'id7',  'id8',  'id9',  'id10',  'id11',  'id12',  'id13',  'id14',  'id15',
  'id16',  'id17',  'id18',  'id19',  'id20'],
 'Y': [26,  21, 49,  28,  22,  31,  33,  45,  42,  27,  11,  12,  42,  43,  36,  43,  30,  41,  16,  21]}

Then understandably, the original solution does not work since the logic is no longer correct.

In the edit to my reply, what I wanted to know was this: how can I use the above time column (using inputs from the slider) to filter the data points displayed on the plot?

I want the CustomJS script to see the range provided by the range slider, and show only those values of X,Y for which the Time value lies in the range.

Thanks

Here are some links related to your question:

If you want to show only the active x-axis labels, you can use this code:

from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import RangeSlider, CustomJS, ColumnDataSource
from bokeh.layouts import column

sample_data = {'Time': [8, 9, 10, 11, 12, 12, 12, 13, 14, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 14],
                'Food': ['eggs', 'eggs', 'eggs', 'eggs', 'eggs', 'eggs', 'banana', 'banana', 'banana', 'banana',
                             'banana', 'milk', 'milk', 'milk', 'milk', 'milk', 'sugar', 'sugar', 'sugar', 'sugar'],
                'X': ['id1', 'id2', 'id3', 'id4', 'id5', 'id6', 'id7', 'id8', 'id9', 'id10', 'id11', 'id12',
                      'id13', 'id14', 'id15','id16', 'id17', 'id18', 'id19', 'id20'],
                'Y': [26, 21, 49, 28, 22, 31, 33, 45, 42, 27, 11, 12, 42, 43, 36, 43, 30, 41, 16, 21]}

source = ColumnDataSource(sample_data)

TOOLTIPS= [
    ("X","@X"),
    ("Y", "@Y"),
    ("Food", "@Food"),
    ("Time", "@Time"),
    ]

plot = figure(x_range = source.data['X'],tooltips = TOOLTIPS, sizing_mode = 'stretch_width')

circles = plot.circle(x="X", y="Y", source=source)

# Set up Slider widget
number_slider = RangeSlider(
    start=min(sample_data['Time']),
    end=max(sample_data['Time']),
    step = 1,
    value=(min(sample_data['Time']), max(sample_data['Time'])),
    title="Select time window",
)

# Set up CustomJS callback
custom_js = CustomJS(
    args=dict(plot=plot, source=source),
    code="""
    const [start, end] = cb_obj.value
    const time = source.data["Time"]
    const x_ini = source.data["X"]
    const bool = Array.from(time).map(d => (d >= start && d <= end))
    plot.x_range.factors = x_ini.filter((elt, index) => bool[index])
    """
)

# Add callback to slider widget
number_slider.js_on_change("value", custom_js)

layout = column([number_slider, plot], sizing_mode="stretch_width")
show(layout)

If you just want to remove the unselected glyphs but keep all initial axis labels, you can use a CDSView:

from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import RangeSlider, CustomJS, ColumnDataSource, CDSView, BooleanFilter
from bokeh.layouts import column

sample_data = {'Time': [8, 9, 10, 11, 12, 12, 12, 13, 14, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 14],
                'Food': ['eggs', 'eggs', 'eggs', 'eggs', 'eggs', 'eggs', 'banana', 'banana', 'banana', 'banana',
                             'banana', 'milk', 'milk', 'milk', 'milk', 'milk', 'sugar', 'sugar', 'sugar', 'sugar'],
                'X': ['id1', 'id2', 'id3', 'id4', 'id5', 'id6', 'id7', 'id8', 'id9', 'id10', 'id11', 'id12',
                      'id13', 'id14', 'id15','id16', 'id17', 'id18', 'id19', 'id20'],
                'Y': [26, 21, 49, 28, 22, 31, 33, 45, 42, 27, 11, 12, 42, 43, 36, 43, 30, 41, 16, 21]}

bool_filter = BooleanFilter([True] * len(sample_data["Time"]))

view = CDSView(filter=bool_filter)

source = ColumnDataSource(sample_data)

TOOLTIPS= [
    ("X","@X"),
    ("Y", "@Y"),
    ("Food", "@Food"),
    ("Time", "@Time"),
    ]

plot = figure(x_range = source.data['X'],tooltips = TOOLTIPS, sizing_mode = 'stretch_width')

circles = plot.circle(x="X", y="Y", source=source, view=view)

# Set up Slider widget
number_slider = RangeSlider(
    start=min(sample_data['Time']),
    end=max(sample_data['Time']),
    step = 1,
    value=(min(sample_data['Time']), max(sample_data['Time'])),
    title="Select time window",
)

# Set up CustomJS callback
custom_js = CustomJS(
    args=dict(plot=plot, source=source, bool_filter=bool_filter),
    code="""
    const [start, end] = cb_obj.value
    const time = source.data["Time"]
    const x_ini = source.data["X"]
    bool_filter.booleans = Array.from(time).map(d => (d >= start && d <= end))
    // Uncomment this line if you want to hide inactive values on the x-axis:
    // plot.x_range.factors = x_ini.filter((elt, index) => bool_filter.booleans[index])
    source.change.emit()
    """
)

# Add callback to slider widget
number_slider.js_on_change("value", custom_js)

layout = column([number_slider, plot], sizing_mode="stretch_width")
show(layout)
1 Like

Your first solution worked perfectly! Thank you so much!

1 Like