Skip to content

Creating figure with tile and range breaks reset range #14613

@elinaolfat

Description

@elinaolfat

Software versions

Python version        :  3.12.11 (main, Jun 12 2025, 12:44:17) [MSC v.1943 64 bit (AMD64)]
IPython version       :  (not installed)
Tornado version       :  6.5.1
NumPy version         :  1.26.4
Bokeh version         :  3.7.3
BokehJS static path   :  C:\Users\eolfat\OneDrive - Echodyne\Desktop\sample data\.venv\Lib\site-packages\bokeh\server\static
node.js version       :  (not installed)
npm version           :  (not installed)
jupyter_bokeh version :  (not installed)
Operating system      :  Windows-11-10.0.26100-SP0

Browser name and version

No response

Jupyter notebook / Jupyter Lab version

No response

Expected behavior

In my original code, there's an instance where I have to recreate my map figure and reapply the ranges it had prior to recreating it. This is because the user might have zoomed-in on the plot, and I want the recreated figure to show the same zoomed-in view, while still allowing the user to reset these ranges using Bokeh's reset tool (reset back to whatever ranges the figure had before zooming). My figure is created with Mercator type axes, tiles, and defined ranges.

If you look at the example code provided here: On both plots (No tile, and With tile), zooming/panning then clicking the Recreate button, then clicking the reset tool from the bokeh toolbar should reset the ranges to the original ranges (the view it started with).

Observed behavior

I realized while this series of actions (zoom -> recreate -> reset tool) works as expected on the figures without a tile provider, it does not reset to the original ranges on the plot with tile provider.

Looking at the example code I provided, the "No tile" plot works as expected. Meaning after zooming then clicking Recreate, the zoom range remains preserved with whatever way it was prior to clicking the Recreate button, and clicking the reset tool from the bokeh toolbar properly resets the ranges to the original un-zoomed ranges.

However, this does not work when the plot figure is created with tiles. If you zoom in on the "With tile" plot and then hit Recreate, the ranges do remain preserved but clicking the reset tool won't reset it back to its original ranges. In fact, if you zoom in and out from that state, clicking the reset tool will bring the ranges back to whatever it was AFTER clicking the Recreate button. This means the user has no way of going back to the original un-zoomed ranges.

As the example code shows, the only difference between how the two plots are created is that one is created with tiles. I've noticed this issue happens when the figure is created with explicit x_range and y_range, and tile is added. If tile is added to a plot that does not have explicitly defined x_range and y_range upon creation, it no longer seems to have this issue.

Example code

from bokeh.plotting import figure, curdoc
from bokeh.models import Button, Column, RadioButtonGroup, ResetTool, PanTool, Row, WheelZoomTool

# Global state
current_fig = None
main_layout = None

def create_map(with_tile=False):
    """Create mercator map"""
    fig = figure(
        title="No tile",
        tools=[PanTool(), WheelZoomTool(), ResetTool()],
        x_axis_type="mercator",
        y_axis_type="mercator",
        x_range=(0, 5),
        y_range=(0, 5),
    )

    if with_tile:
        fig.title.text="With tile"
        fig.add_tile("ESRI WorldGrayCanvas")

    fig.scatter([1,2,3,4], [1,2,3,4], size=8, alpha=0.7, color='red')
    return fig

def toggle_plot(attr, old, new):
    global current_fig, main_layout
    current_fig = create_map(new == 1)
    main_layout.children[1] = current_fig

def recreate_plot():
    global current_fig, main_layout
    
    # Store ranges
    zoom_ranges = {
        "x_range": (current_fig.x_range),
        "y_range": (current_fig.y_range),
    }

    # Recreate figure
    if current_fig.title.text == "No tile":
        current_fig = create_map()
    else:
        current_fig = create_map(True)

    # Restore ranges
    current_fig.x_range = zoom_ranges["x_range"]
    current_fig.y_range = zoom_ranges["y_range"]
    
    main_layout.children[1] = current_fig

# Create widgets
toggle_button = RadioButtonGroup(labels=["No tile", "With tile"], active=0)
toggle_button.on_change('active', toggle_plot)

recreate_button = Button(label="Recreate")
recreate_button.on_click(recreate_plot)

# Initialize
current_fig = create_map()
main_layout = Column(Row(toggle_button, recreate_button), current_fig)

curdoc().add_root(main_layout)

Stack traceback or browser console output

No response

Screenshots

No response

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions