Trying to make an interactive map where the selection point moves using sliders

I making an interactive map so users can select a location to get data for a crop yield tool. I have the map and I have sliders showing the latitude and longitude and the initial point shows up but when I move the sliders the point disappears

Shortened version:

globe_points = {'x':[list of latitudes], 'y':[list of longitudes]} # each is ~27500 long
web_points = {'x':[list of lats converted to web mercator], 
              'y':[list of long converted to web mercator]}
       
web_plot = ColumnDataSource(data=web_points)
globe_loc = ColumnDataSource(data=globe_points)
latitude_slider = Slider(start=min_lat, end=max_lat, 
                         value=globe_points['x'][14250],
                         step=.1, 
                         title="Latitude")
longitude_slider = Slider(start=min_long, end=max_long, 
                          value=globe_points['y'][10000],
                          step=.1, title="Longitude")

#x and y coordinates for the point on the map
source = {'x':[web_plot.data['x'][14250]], 
          'y':[web_plot.data['y'][10000]]} #starting plot point

#The below creates the bounding box for the map
pts = [(min_lat, min_long), (max_lat, max_long)]
bbox = [ ]
#convert from lat/long to web mercator using pyproj
for pt in transformer.itransform(pts): 
    bbox.append(pt)    

#Use the bounding box to identify the map plot area
p = figure(x_range=(bbox[0][0], bbox[1][0]),
           y_range=(bbox[0][1], bbox[1][1]),
           x_axis_type="mercator", y_axis_type="mercator")
#add the map from the Bokeh map vendor in this case Stamen_Terrain
p.add_tile(tile_provider)
#plots the initial point we want the user to move
p.circle('x','y',source=source, color= "blue")

callback = CustomJS(args=dict(source=source, 
                    web_plot=web_plot,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        const web = web_plot.data
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        var idx_lat = globe.x.indexOf(X);
                        var idx_long = globe.y.indexOf(Y);
                        data.x[0]=web.x[idx_lat]
                        data.y[0]=web.y[idx_long]
                        source.change.emit();
                    """)

latitude_slider.js_on_change('value', callback)
longitude_slider.js_on_change('value', callback)

layout = row(p, column(latitude_slider, longitude_slider))
show(layout)

Hi @tpike3 please 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)

@Bryan Done, sorry about that I was rushing. Also apologies for the code length, but after several variation attempts I thought it was import to share all the pieces.

@tpike3

Do you see any errors in the JavaScript console of the browser when you manipulate the sliders?

Without a complete example to run to reproduce, I would look there for a clue.

From an inspection of the code as posted, I might hypothesize that your slider selections do not correspond to actual entries in either the globe.x or globe.y arrays. In which case your callback logic for indexOf() returns -1. Then, if you use this to access either web.x[-1] or web.y[-1] you get undefined behavior.

@_jm Thanks, that actually gets to the challenge I am having I can’t figure out how to debug that code. I set up a pure javascript version and it worked but I am debugging by inference. The only hint I got right now is whether note the slider value updates. Its seems how I set up the source variable is the key piece but I can’t figure out why. Two code snippet variations below.

Under variation 1: the slider moves, but the values don’t change, dot doesn’t move

#web_points is the dictionary of web mercator locations
source = ColumnDataSource(data=dict(x=[web_points['x'][14250]], y=[web_points['y'][10000]]))
callback = CustomJS(args=dict(source=source, 
                    web_plot=web_plot,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        const lat = data['x']
                        const long = data['y']
                        const web = web_plot.data
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        var idx_lat = globe.x.indexOf(X);
                        var idx_long = globe.y.indexOf(Y);
                        lat=[web.x[idx_lat]]
                        long=[web.y[idx_long]]
                        source.change.emit();
                    """)

Under variation 2: slide values update but doesn’t move

def convert(lat,long): 
    return transformer.transform(lat,long)

pre_location = convert(latitude_slider.value, longitude_slider.value)
source = ColumnDataSource(data=dict(x=[pre_location[0]], y=[pre_location[1]]))
callback = CustomJS(args=dict(source=source, 
                    web_display=web_display,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        const web = web_display.data;
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        var idx_lat = globe.x.indexOf(X);
                        var idx_long = globe.y.indexOf(Y);
                        data.x[0]=web.x[idx_lat]
                        data.y[0]=web.y[idx_long]
                        source.change.emit();
                    """)

@tpike3

The JavaScript code of the callback is running in the web browser. Depending on the web browser you use, there are different ways to bring up the console to debug, see what is going on and what errors, if any, are being generated, etc.

For Google Chrome, you can right-click on the webpage, and select Inspect from the menu. There should appear an interface with tabs, one of which is labeled Console that will report things including presumably errors after you manipulate the slider(s).

Alternatively, if you can provide a minimally reproducible example, i.e. something that can be cut-and-pasted for others to run, it might be possible to provide more concrete recommendations on what to fix.

Thanks, I will give that whirl ( I am new to JS) Below is the code I was using to test my JavaScript:

const data = {'x':[1.2],'y':[2.4]};
const web = {'x':[3.6,9.8],'y':[5.2,5.4]}
const globe = {'x':[45.2,67.3],'y':[23.4,87.2]};
const X = globe.x[0];
const Y = globe.y[1];
var idx_lat = globe.x.indexOf(X);
var idx_long = globe.y.indexOf(Y);
data.x[0]=web.x[idx_lat]
data.y[0]=web.y[idx_long]
//source.change.emit();
document.getElementById("demo").innerHTML = data['y'];

@_jm I looked at the callback in the webbrowser and I am getting:

Uncaught Error: Model 'Slider' does not exist. This could be due to a widget or a custom model not being registered before first usage.

Which is weird because the slide is working and the values are updating

IIRC it may happen if you manage Bokeh resources manually and don’t include the JavaScript file for bokeh-widgets.

@p-himik Thanks, how do I include the bokeh-widgets? I am using pycharm community and installed bokeh via pip.

If you don’t touch Bokeh resources then they should be included automatically.
If that’s not the case, please create a minimal reproducible example where that exact error can be seen.

I have gotten rid of all the errors, I just cannot get the dot to move. Slider values update but somehow that is not updating my source. callback code is below:

callback = CustomJS(args=dict(source=source, 
                    web_plot=web_plot,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        var lat = data['x'];
                        var long = data['y'];
                        const web = web_plot.data;
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        const idx_lat = globe.x.indexOf(X);
                        const idx_long = globe.y.indexOf(Y);
                        lat=[web['x'][idx_lat]];
                        long=[web['y'][idx_long]];
                        source.change.emit();
                    """)

My assessment is the update from the slider is not talking to source but the minimal code works in a JS minimal example. Not sure why, at this point I have tried to replace just the value at the index and the entire array nothing seems to do it.

Your callback just reassigns the lat and long variables - it doesn’t communicate the changes back to the data source. You need something like source.data = {x: lat, y: long}; (if you assign the data attribute I think you can even remove the source.change.emit(); line).

@p-himik Tried several variations and nothing will move that dot:

callback = CustomJS(args=dict(source=source, 
                    web_plot=web_plot,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        const web = web_plot.data;
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        const idx_lat = globe.x.indexOf(X);
                        const idx_long = globe.y.indexOf(Y);
                        const lat = web['x'][idx_lat];
                        const long = web['y'][idx_long];
                        data = {x: [lat], y: [long]};
                        source.change.emit();
                    """)

I tried this with and without the source.change.emit()

I also tried an inefficient way with loops

callback = CustomJS(args=dict(source=source, 
                    web_plot=web_plot,
                    globe_loc=globe_loc,
                    latitude_slider=latitude_slider, 
                    longitude_slider=longitude_slider),
                    code=""" 
                        const data = source.data;
                        const web = web_plot.data;
                        const globe = globe_loc.data;
                        const X = latitude_slider.value;
                        const Y = longitude_slider.value;
                        const idx_lat = globe.x.indexOf(X);
                        const idx_long = globe.y.indexOf(Y);
                        const lat = data['x'];
                        const long = data['y'];
                        for (var i = 0; i < globe.x.length; i++) {
                            if (i == idx_lat){
                                lat[0]= web['x'][idx_lat];
                                }
                        }
                        for (var i = 0; i < globe.y.length; i++){
                            if (i == idx_long){
                            long[0] = web['y'][idx_long]
                            }
                        }
                        source.change.emit();
                    """) 

I modeled it on the demo code which does this and works perfectly in my environment

allback = CustomJS(args=dict(source=source, amp=amp_slider, freq=freq_slider, phase=phase_slider, offset=offset_slider),
                    code="""
    const data = source.data;
    const A = amp.value;
    const k = freq.value;
    const phi = phase.value;
    const B = offset.value;
    const x = data['x']
    const y = data['y']
    for (var i = 0; i < x.length; i++) {
        y[i] = B + A*Math.sin(k*x[i]+phi);
    }
    source.change.emit();
""")

Not sure about the rest of the examples but in your first example you’re doing this

data = {x: [lat], y: [long]};

and I have suggested to do this, assuming that lat and long are already arrays:

source.data = {x: lat, y: long};

Do you see the difference?

If you still can’t figure it out, create a minimal reproducible example, with all the imports and some test data, and I’ll take a look.

@tpike3

And also note from the original code excerpt that you specified source as a dict here

and are using it as if it is a ColumnDataSource in your callback. I suspect you’d see an error in the JS console of the browser if that’s still the case.

To the original point, I also think the indexing into the data with the slider value and indexOf() can cause problems with a -1 return value unless your data is very specifically constructed to be consistent with the step size of the sliders, etc.

A minimal example with imports, data, etc. will help disambiguate where the problem(s) is(are).

@_jm and @p-himik Thanks again for the help. An attempt at a minimal example below but this is giving me an error I am not getting with the actual code;

from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, output_notebook, show
#from bokeh.tile_providers import get_provider, Vendors
#tile_provider = get_provider('STAMEN_TERRAIN')
output_notebook()

odd_locs = {'x': list(range(1,10,2)), 'y': list(range(1,10,2))}
even_locs = {'x': list(range(0,10,2)), 'y':list(range(0,10,2))}
source = {'x':[2], 'y':[2]}
x_slider = Slider(start=0, end=10, value=0, step = 2, title="X_Slider")
y_slider = Slider(start=0, end=10, value=0, step =2, title="Y_Slider")

p = figure(x_range=(0,10),y_range=(0,10))
p.circle('x','y',source=source, color= "blue")
callback = CustomJS(args=dict(source=source, 
                    odd_locs = odd_locs,
                    even_locs = even_locs,
                    x_slider=x_slider, 
                    y_slider=y_slider),
                    code=""" 
                        var data = source.data;
                        const odd = odd_locs.data;
                        const even = even_locs.data;
                        const red = x_slider.value;
                        const blue = y_slider.value;
                        const idx_lat = even.x.indexOf(red);
                        const idx_long = even.y.indexOf(blue);
                        const lat = odd['x'][idx_lat];
                        const long = odd['y'][idx_long];
                        data = {'x': [lat], 'y': [long]};
                        source.change.emit();
                    """)
x_slider.js_on_change('value', callback)
y_slider.js_on_change('value', callback)

layout = row(p, column(x_slider, y_slider))
show(layout)

Error is
Uncaught TypeError: Cannot read property 'x' of undefined
and it is at
const idx_lat = even.x.indexOf(red);
And I am new to Javascript but this code doesn’t error an a JS interpreter I am testing on

@tpike

That error is related to the following two lines of your callback. It throws an error b/c even_locs is defined as a dictionary in the body of your Python code, but in the callback you’re treating it as if it is a ColumnDataSource and trying to access its data property.

Yep just realized I forgot the Bokeh ColumnDataSource. Let me fix that and see if I get it working

@tpike3

This version with minimal modifications to your script works for me. I wrapped all of the dicts in ColumnDataSource and changed the assignment from data to source.data in the callback.

from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, output_notebook, show
#from bokeh.tile_providers import get_provider, Vendors
#tile_provider = get_provider('STAMEN_TERRAIN')
output_notebook()

odd_locs = ColumnDataSource(data={'x': list(range(1,10,2)), 'y': list(range(1,10,2))})
even_locs = ColumnDataSource(data={'x': list(range(0,10,2)), 'y':list(range(0,10,2))})
source = ColumnDataSource(data={'x':[2], 'y':[2]})
x_slider = Slider(start=0, end=10, value=0, step = 2, title="X_Slider")
y_slider = Slider(start=0, end=10, value=0, step =2, title="Y_Slider")

p = figure(x_range=(0,10),y_range=(0,10))
p.circle('x','y',source=source, color= "blue")
callback = CustomJS(args=dict(source=source, 
                    odd_locs = odd_locs,
                    even_locs = even_locs,
                    x_slider=x_slider, 
                    y_slider=y_slider),
                    code=""" 
                        var data = source.data;
                        const odd = odd_locs.data;
                        const even = even_locs.data;
                        const red = x_slider.value;
                        const blue = y_slider.value;
                        const idx_lat = even.x.indexOf(red);
                        const idx_long = even.y.indexOf(blue);
                        const lat = odd['x'][idx_lat];
                        const long = odd['y'][idx_long];
                        source.data = {'x': [lat], 'y': [long]};
                        source.change.emit();
                    """)
x_slider.js_on_change('value', callback)
y_slider.js_on_change('value', callback)

layout = row(p, column(x_slider, y_slider))
show(layout)