I have a plot that’s updated with a RangeTool, but I have a problem. I want to enforce a rule that end-start = 5. In other words, the selected band is always 5 wide. For the life of me, I can’t figure out a way to do it.
I have a CustomJS callback that’s triggered on a start or end event. I thought I could access the prior state and do a difference to figure out which value has changed (start or end) and then update the other number appropriately, but I can’t figure out how to access the prior state.
I thought I could access the state of the plot I’m altering and check the x_ranges, but that doesn’t work because they always seem to be updated before the callback is called.
Does anyone know a way to enforce a constant width for RangeTool?
I guess I am not seeing why you’d need the prior state. If end changes and width is not 5, adjust start appropriately to make the width 5. If start changes and the width is not 5, change end appropriately to make the width 5. You don’t need to “figure out” which changed in order to do that, you just need different CustomJS callbacks for start and end that do the right things in each case. In either callback, if the width is already 5, do nothing (to prevent infinite callback ping-pong) [1]
Perhaps you can share a complete Minimal Reproducible Example to focus the discussion on actual code.
I’m assuming you’re in a range where floating point effects don’t come into play… in general you might have no choice except to settle for “5 +/- some epsilon tolerance” ↩︎
Claude code gen + Cursor to the rescue! Here’s some Claude generated code that I fixed with Cursor. This does more or less what I want it to do.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.layouts import column
from bokeh.models import RangeTool, CustomJS
from bokeh.io import curdoc
# Generate sample data
N = 300
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x) + 0.1*np.random.randn(N)
y2 = np.cos(x) + 0.1*np.random.randn(N)
# Create the main plot
main_plot = figure(
width=800,
height=400,
title="Main Plot - Zoomed View",
tools="pan,wheel_zoom,box_zoom,reset,save"
)
# Add line renderers to main plot
main_line1 = main_plot.line(x, y, line_width=2, color='blue', alpha=0.8, legend_label="sin(x)")
main_line2 = main_plot.line(x, y2, line_width=2, color='red', alpha=0.8, legend_label="cos(x)")
main_plot.legend.click_policy = "hide"
# Create the overview plot (smaller)
overview_plot = figure(
width=800,
height=200,
title="Overview Plot - Use RangeTool to select range",
tools="",
toolbar_location=None
)
# Add the same data to overview plot
overview_plot.line(x, y, line_width=1, color='blue', alpha=0.6)
overview_plot.line(x, y2, line_width=1, color='red', alpha=0.6)
# Define the constant range width
RANGE_WIDTH = 2.0 # This is the constant width we want to maintain
# Calculate initial range
initial_start = np.pi
initial_end = initial_start + RANGE_WIDTH
# Create RangeTool with initial range
range_tool = RangeTool(
x_range=main_plot.x_range
)
# Add the range tool to overview plot
overview_plot.add_tools(range_tool)
overview_plot.toolbar.active_multi = range_tool
# JavaScript callback to enforce constant width
callback_code = f"""
const range_width = {RANGE_WIDTH};
const x_range = cb_obj;
// Get current start and end
let start = x_range.start;
let end = x_range.end;
let current_width = end - start;
// Only adjust if the width has changed significantly
if (Math.abs(current_width - range_width) > 0.001) {{
// Determine which end moved more and adjust accordingly
let center = (start + end) / 2;
let new_start = center - range_width / 2;
let new_end = center + range_width / 2;
// Update the range while avoiding infinite recursion
x_range.setv({{
'start': new_start,
'end': new_end
}});
}}
"""
# Add the callback to the main plot's x_range
main_plot.x_range.js_on_change('start', CustomJS(code=callback_code))
main_plot.x_range.js_on_change('end', CustomJS(code=callback_code))
# Set initial range on main plot
main_plot.x_range.start = initial_start
main_plot.x_range.end = initial_end
# Create layout
layout = column(main_plot, overview_plot)
# Alternative method using Python callback (requires bokeh serve)
def python_callback(attr, old, new):
"""Python callback to enforce constant width - requires bokeh serve"""
current_start = main_plot.x_range.start
current_end = main_plot.x_range.end
current_width = current_end - current_start
if abs(current_width - RANGE_WIDTH) > 0.001:
center = (current_start + current_end) / 2
main_plot.x_range.start = center - RANGE_WIDTH / 2
main_plot.x_range.end = center + RANGE_WIDTH / 2
# Uncomment these lines if using with bokeh serve
# main_plot.x_range.on_change('start', python_callback)
# main_plot.x_range.on_change('end', python_callback)
# Show the plot
show(layout)
# If running with bokeh serve, use this instead:
# curdoc().add_root(layout)
print("Bokeh RangeTool Demo with Constant Width")
print(f"Range width is fixed at: {RANGE_WIDTH}")
print("\nTo run this demo:")
print("1. Save as 'rangetool_demo.py'")
print("2. Run: python rangetool_demo.py (opens in browser)")
print("3. Or run: bokeh serve rangetool_demo.py (for live server with Python callbacks)")
print("\nTry dragging the range selector in the overview plot!")
print("The selection will maintain a constant width as you move it around.")
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.