DataRange1d update problems

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).

datarange

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:

pan

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:

  1. 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.

  2. Altering other properties of the y_range in the callback → bounds, min/max, follow, etc. Never found one that does what I’m after.

  3. 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…

  4. 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')

Just wanted to let you know that I’ve experienced the exactly same behavior (over a number of bokeh versions), including the weird part of the update working after panning manually (I learned the latter from my users who had gotten used to do the panning before entering a number for the range after noticing the glitch). Never resolved this as for an internally used tool so far it was sufficient to know the workaround with panning.

1 Like

I think this is just a current limitation of data ranges. They are overridden by an initial custom value, but without that, they currently always compute new start and end. I suppose you could try setting _initial_end yourself (in addition to setting end), but all the standard warnings apply: it is private, undocumented, and may change at any time.

I think a feature request / new development will be needed for there to be a supported mechanism to selectively override auto-ranging after initialization. Offhand, maybe the private _initial_start could just become a public override_start and that is all it would take. But data ranges are literally one of the most complicated things in all of Bokeh, so it will definitely require some care and testing.

Thanks for the response. Just for consolidation of my understanding: it’s the _initial_end/start that are altered when the user does a pan/wheelzoom etc, and this is why my second gif behaves the way it does?

I think a feature request / new development will be needed for there to be a supported mechanism to selectively override auto-ranging after initialization.

If wholesale swapping of Ranges on figure were supported this would not be needed → you’d just instantiate a Range1D, pass it to the callback, alter its start and end, then assign it as the figure’s x/y_range. Instead of having “_initial_start/end” on the range, the figure would have “_initial_range”, which would point to the range object the figure is first instantiated with, and that’d get triggered on reset event. Does that make sense as an idea?

As usual I have very little understanding of the roadmap to accomplish such a thing… and I know ranges have already been beaten to death in terms of effort, so I’m just spit-balling ideas here. I’ll move this to a GH discussion if you think it’s a talk worth having.

No this is not the case. I believe [1] both cases are identical in that the CDS data change causes the range end to be recomputed. But then, in the second case you react to the recomputed end value, and overwrite it with the actual value you want. Et voila.

If wholesale swapping of Ranges on figure were supported this would not be needed

Ish. It’s still heavyweight in terms of work that would have to be done at runtime, and now we’d have to explain that “make the smallest change possible” is the best-practice for using Bokeh, except in this or that special circumstance… and that’s never really a good place to be in documentation-wise.

As always this comes down to history. When data ranges were added, Bokeh had no notion or way to express "read-only’ properties that are only ever computed in the JS side of things. If there had been, we probably would have had computed_start and computed_end be read-only properties that just always report the current computed data-range, have start and end be properties that uses can optionally set to override computed values. But that was not possible, so we tried to combine use all those cases into just start and end which mostly suffices, except when it doesn’t.

Since it would be too disruptive to make a change to what I just described, what I think would work is to make a new override_start and override_end that can be optionally set to override the always-computed start and end (at any point, not just initial) instead (since adding new properties is less disruptive).


  1. Modulo all of this by the fact that, in addition to being the gorpiest, most-complicated corner of BokehJS, I have not really looked at this code in detail in several years ↩︎