How to interactively change TileProvider with a CustomAction Tool?

Hi,

I have a figure showing some Geo Data with the CARTODBPOSITRON_RETINA tile. I like to implement an interactive way to toggle between CARTODBPOSITRON_RETINA and ESRI_IMAGERY. So far, I could not find any resource on how to do this. Since I want to have this without a bokeh python server running, I guess I need to use the CustomAction Tool and add it as a tool directly to the plot.
As far as I know, this needs to be done within a CustomJS.
Unfortunalty, I am very new to JavaScipt, but I figured out how to access the renderes of the plot.
In the example below, the console outputs the two renderers of the plot (TileRenderer as well as the Glyphs).

My question is two-fold:
First, is the idea of using a CustomAction Tool the the correct one?
And Seconds: How can I access, toggle and replace the two different tiles within the JSCallback on clicking my CustomAction Tool?

Minimal Working Example of a plot using the custom Tool:

import pandas as pd
import numpy as np
from PIL import Image

from bokeh.layouts import Column
from bokeh.plotting import curdoc
from bokeh.plotting import figure
from bokeh.tile_providers import CARTODBPOSITRON_RETINA, ESRI_IMAGERY, get_provider
from bokeh.models import CustomAction
from bokeh.models.callbacks import CustomJS
from bokeh.io import output_notebook, show

fig_map = figure(plot_width=800, plot_height=800,
                        x_axis_type="mercator", 
                        y_axis_type="mercator", 
                        x_range=(589993, 1725452), 
                        y_range=(5860839, 7459516),
                        output_backend="webgl")

tile_provider_sat = get_provider(ESRI_IMAGERY)
tile_provider_street = get_provider(CARTODBPOSITRON_RETINA)

fig_map.add_tile(tile_provider_street)

df = pd.DataFrame({'x':[1.421437e+06,1.420402e+06, 1.419831e+06], 
                   'y':[6.572054e+06, 6.570864e+06,6.569402e+06]})
r_map = fig_map.circle(x='x', y='y', source=df) 

cb_tile_map = CustomJS(args=dict(fig_map=fig_map, tile_provider_sat=tile_provider_sat, tile_provider_street=tile_provider_street), 
                            code="""
                            console.log(fig_map.renderers);
                            fig_map.renderers[0] = tile_provider_sat;
                            """)

tile_tool = CustomAction(name='TileTool', callback= cb_tile_map, icon = Image.fromarray(np.random.random(size=(100,100,3)), 'RGB'))
fig_map.add_tools(tile_tool)
show(fig_map)

Thanks for the help.

The best approach is probably to add all the tile renderers you will want up front, then use the CustomJS to toggle their visible property values as appropriate.

Tanks @Bryan.

The Idea of just setting the visibility is great.
In case anybody wants to reuse this, here is a working example:

import pandas as pd
import numpy as np
from PIL import Image
from bokeh.layouts import Column
from bokeh.plotting import curdoc
from bokeh.plotting import figure
from bokeh.tile_providers import CARTODBPOSITRON_RETINA, ESRI_IMAGERY, get_provider
from bokeh.models import CustomAction
from bokeh.models.callbacks import CustomJS
from bokeh.io import output_notebook, show

fig_map = figure(plot_width=800, plot_height=800,
                        x_axis_type="mercator", 
                        y_axis_type="mercator", 
                        x_range=(589993, 1725452), 
                        y_range=(5860839, 7459516),
                        output_backend="webgl")

tile_provider_sat = get_provider(ESRI_IMAGERY)
tile_provider_street = get_provider(CARTODBPOSITRON_RETINA)
fig_map.add_tile(tile_provider_sat)
fig_map.add_tile(tile_provider_street)
fig_map.renderers[1].visible=False

df = pd.DataFrame({'x':[1.421437e+06,1.420402e+06, 1.419831e+06], 
                   'y':[6.572054e+06, 6.570864e+06,6.569402e+06]})
r_map = fig_map.circle(x='x', y='y', source=df) 

cb_tile_map = CustomJS(args=dict(fig_map=fig_map, tile_provider_sat=tile_provider_sat, tile_provider_street=tile_provider_street), 
                            code="""
                            fig_map.renderers[0].visible = !fig_map.renderers[0].visible;
                            fig_map.renderers[1].visible = !fig_map.renderers[1].visible;
                            """)

tile_tool = CustomAction(name='TileTool', callback=cb_tile_map, icon=Image.fromarray(np.random.random(size=(100,100,3)), 'RGB'))
fig_map.add_tools(tile_tool)
fig_map.xgrid.visible=False
fig_map.ygrid.visible=False
show(fig_map)

Note that this callback doesn’t check, if renderers[0] and renderers[1] are actually the TileRenderes. In case this list gets mixed up, it might lead to unwanted behaviour. Anyways, the example is only to show the working idea.
Thanks.

1 Like

FYI I would pass the tile renderers (returned by add_tile) in the args dict so they could be manipulated by name (rather than pulling out of fig.renderers)