What are you trying to do?
I have a CDS driving a renderer on a figure. The CDS is being updated dynamically on mouse movement. The MRE (made to be as simple as possible) simply updates the CDS’s data to be set equal to itself and emitted on mousemove → in my real use case obviously more happens but yeah.
For my use case it is also imperative that the figure’s y_range be initialized with a DataRange1d that follows the renderer.
Then I’ve got a NumericInput model where I’m trying to let the user update the y_range end value.
The simple goal for my MRE is to allow the user to change the value in the NumericInput, which will “hard” set the end value for the y_range to stay there.
What have you tried that did NOT work as expected? If you have also posted this question elsewhere (e.g. StackOverflow), please include a link to that post.
When the user changes the number, the y_range update executes as desired. HOWEVER, when the user mouses back over the figure after doing so, the y_range “snaps back” to follow the datasource. This is because the datasource gets changed via the mousemove callback, and this change triggers the DataRange1d to recalculate the y_range start/end (which I don’t want).
Now where things get weird:
If I “manually” execute a pan event on the figure first, the update behaves the way I want, i.e. the y_range updates, and stays there even when the source changes on mousemove:
I tried this with a “manual” wheel zoom event as well and got the same thing. So what must be happening (in the built-in bokeh-side of things), is the pan event or wheel zoom event does something to the y_range so that it no longer “listens to” or follows the source. It’s like the pan event transforms the DataRange1d into a Range1d… but if I console.log(f.y_range.type) in the MRE it’ll always report DataRange1d…
So there’s some property I don’t know about that I need to update to get the behaviour I’m after and all I know is that pan event/pan action/wheelzoom event/wheelzoom action on the plot updates it
I’ve spent most of the day looking for workarounds and diving down rabbit holes. I’ve tried:
-
Passing a Range1d model to the CustomJS, setting its start/end properties appropriately, and assigned it to the x_range of the figure. That has no effect, and I have read some GH discussions/issues pointing out that this sort of thing (i.e. wholesale swapping of ranges on figures) is not supported.
-
Altering other properties of the y_range in the callback → bounds, min/max, follow, etc. Never found one that does what I’m after.
-
Trying to mimic/manufacture a Pan or Wheelzoom event to mimic that manual step in the second gif, akin to Manufacturing Tap Events . Pretty janky even if I could get it to work…
-
Finally I came up with adding a callback to the y_range, to trigger whenever its end property changes, that will hard-force its end to be equal to the NumericInput value if the numeric Input value is not null. This kinda does what I want, but if the user wants to go back to “free pan/zoom” they’ll need to set the NumericInput to null, AND I’d have to make a few other callbacks ensuring the NumericInput’s value gets nulled when reset buttons get pressed etc.
I’m using 3.0.3, and I did read a lot about the RangesUpdate event being a very recent addition… I don’t think that it really applies to what I’m trying to do here, but it might.
Thoughts/advice solicited… thanks…
My MRE:
from bokeh.plotting import figure,save
from bokeh.models import Range1d, DataRange1d, CustomJS, NumericInput, HoverTool, ColumnDataSource,Button, Range1d
from bokeh.layouts import column
from bokeh.events import Pan, RangesUpdate
f = figure(width=600,height=300,tools=['pan','reset','wheel_zoom'],tags=['XS'])
r = f.line(x=[0,500,1000],y=[50,20,10])
f.js_on_event('mousemove',CustomJS(args=dict(r=r)
,code='''
r.data_source.data = r.data_source.data
r.data_source.change.emit()
'''))
ni = NumericInput(placeholder='',title='enter a number')
cb = CustomJS(args=dict(ni=ni,f=f)
,code='''
if (ni.value != null){
f.y_range.end = ni.value
f.y_range.change.emit()}
''')
ni.js_on_change('value',cb)
#"kinda" does what I want
# f.y_range.js_on_change('end',CustomJS(args=dict(ni=ni,f=f),code='''
# if (ni.value !== null){
# f.y_range.end = ni.value
# }
# '''))
save(column([f,ni]),'testing.html')