holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.69k stars 402 forks source link

`match_aspect` parameter for bokeh's `BoxZoomTool` is not working #6211

Open MP-MaximilianLattka opened 5 months ago

MP-MaximilianLattka commented 5 months ago

ALL software version info

Python: 3.11.9 JupyterLab: 4.1.5 Holoviews: 1.18.3 Bokeh: 3.3.4 and 3.4.1

Description of expected behavior and the observed behavior

I’ve been plotting map data using holoviews and geoviews with the bokeh backend.

Bokeh has a zoom tool called BoxZoomTool, which allows drawing a rectangle to zoom in. Setting the kwarg match_aspect=True results in maintaining the aspect ratio, which is meaningful when working with maps. Apparently, holoviews doesn’t properly use pass the BoxZoomTool to bokeh as match_aspect=True does not have any effect.

Complete, minimal, self-contained example code that reproduces the issue

Working BoxZoomTool with native bokeh only (version 3.4.1 and 3.3.4, both working as expected)
import bokeh
from bokeh.plotting import figure, show
from bokeh.models import BoxZoomTool

print(f"{bokeh.__version__=}")

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
counts = [5, 3, 4, 2, 4, 6]

box_zoom = BoxZoomTool(match_aspect=True)

p = figure(x_range=fruits, height=350, title="Fruit Counts",
           toolbar_location=None, tools=[box_zoom])

p.vbar(x=fruits, top=counts, width=0.9)

p.xgrid.grid_line_color = None
p.y_range.start = 0

show(p)
Non-working holoviews code (version 1.18.3)
import numpy as np
import holoviews as hv
from bokeh.models import BoxZoomTool

hv.extension("bokeh")

print(f"{hv.__version__=}")

x,y = np.mgrid[-50:51, -50:51] * 0.1

box_zoom = BoxZoomTool(match_aspect=True)

img = hv.Image(np.sin(x**2+y**2), bounds=(-1,-1,1,1))

(img.relabel('Image') * img.sample(x=0).relabel('Cross-section')).opts(
    tabs=True,
    tools=[box_zoom])
Non-working minimal example of my code (version 1.18.3)
data = [{
    'STATENAME': 'District of Columbia',
    'LAT': (-77.035264, -76.909294, -77.040741, -77.117418, -77.035264),
    'LONG': (38.993869, 38.895284, 38.791222, 38.933623, 38.993869)
}]

box_zoom = BoxZoomTool(match_aspect=True)

tiles = hv.Tiles("https://tile.openstreetmap.org/{Z}/{X}/{Y}.png", name="OSM").opts(
    #"Image", # Does not respect width and height parameter
    width=1400,
    height=800,
    #tools=[box_zoom], # This alone does not preserve match_aspect=True
)

choropleth = gv.Polygons(data, ["LAT", "LONG"], ["STATENAME"]).opts(
    #"Image", # Incompatible with line_color parameter
    xaxis=None,
    yaxis=None,
    line_color="black",
    cmap="bwr",
    alpha=1.0,
    #tools=[box_zoom], # This alone does not preserve match_aspect=True
)
(tiles * choropleth).opts(
    #"Image",
    #tools=[box_zoom], # This alone does not preserve match_aspect=True
)

Stack traceback and/or browser JavaScript console output

Screenshots or screencasts of the bug in action

ahuang11 commented 5 months ago

I believe the issue arises when tabs=True.

A workaround is using Panel:

import panel as pn
import numpy as np
import holoviews as hv
from bokeh.models import BoxZoomTool

pn.extension()
hv.extension("bokeh")

print(f"{hv.__version__=}")

x, y = np.mgrid[-50:51, -50:51] * 0.1

box_zoom = BoxZoomTool(match_aspect=True)

img = hv.Image(np.sin(x**2 + y**2), bounds=(-1, -1, 1, 1))

pn.Tabs(
    img.relabel("Image").opts(default_tools=[box_zoom]),
    img.sample(x=0).relabel("Cross-section"),
)
ahuang11 commented 5 months ago

I think the tools are dropped when tabs=True.

import numpy as np
import holoviews as hv
from bokeh.models import BoxZoomTool

hv.extension("bokeh")

print(f"{hv.__version__=}")

x,y = np.mgrid[-50:51, -50:51] * 0.1

box_zoom = BoxZoomTool(match_aspect=True)

img = hv.Image(np.sin(x**2+y**2), bounds=(-1,-1,1,1))

(img.relabel('Image') * img.sample(x=0).relabel('Cross-section')).opts(
   tabs=True, default_tools=[box_zoom], data_aspect=1, tools=[box_zoom], active_tools=[box_zoom])

i.e. the pan, zoom tools are still here even though I explicitly set everything.

image
ahuang11 commented 5 months ago

I suppose if you specify 'Image', it works:

import numpy as np
import holoviews as hv
from bokeh.models import BoxZoomTool

hv.extension("bokeh")

print(f"{hv.__version__=}")

x,y = np.mgrid[-50:51, -50:51] * 0.1

box_zoom = BoxZoomTool(match_aspect=True)

img = hv.Image(np.sin(x**2+y**2), bounds=(-1,-1,1,1))

(img.relabel('Image') * img.sample(x=0).relabel('Cross-section')).opts(
   "Image", default_tools=[box_zoom], active_tools=[box_zoom]).opts(tabs=True, )
image
MP-MaximilianLattka commented 5 months ago

Specifying "Image" may be a workaround sometimes apparently.

However, I'm working with map data and therefore different objects and this does not suit my issue.

I spent some time creating a minimal example of my particular situation and will update the original description of this issue.