Skip to content

Commit

Permalink
Fix issues with unwrapping longitudes in RangeXY stream (#756)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew <[email protected]>
Co-authored-by: Andrew Huang <[email protected]>
Co-authored-by: Simon Høxbro Hansen <[email protected]>
  • Loading branch information
4 people authored Nov 25, 2024
1 parent da8719e commit e84bf81
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 17 deletions.
4 changes: 2 additions & 2 deletions geoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def project_ranges(cb, msg, attributes):
extents = x0, y0, x1, y1
x0, y0, x1, y1 = project_extents(extents, plot.projection,
plot.current_frame.crs)
if plot._unwrap_lons and -180 <= x0 < 0 or -180 <= x1 < 0:
x0, x1 = x0 + 360, x1 + 360
if plot._unwrap_lons and (-180 <= x0 < 0 or -180 <= x1 < 0):
x1 += 360
if x0 > x1:
x0, x1 = x1, x0
coords = {'x_range': (x0, x1), 'y_range': (y0, y1)}
Expand Down
28 changes: 21 additions & 7 deletions geoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from bokeh.models.tools import BoxZoomTool, WheelZoomTool
from cartopy.crs import GOOGLE_MERCATOR, Mercator, PlateCarree, _CylindricalProjection
from holoviews.core.dimension import Dimension
from holoviews.core.util import dimension_sanitizer
from holoviews.core.util import dimension_sanitizer, match_spec
from holoviews.plotting.bokeh.element import ElementPlot, OverlayPlot as HvOverlayPlot

from ...element import Shape, _Element, is_geographic
Expand Down Expand Up @@ -104,28 +104,42 @@ def _update_ranges(self, element, ranges):
ax_range.end = mid + min_interval/2.
ax_range.min_interval = min_interval

def _set_unwrap_lons(self, element):
def _set_unwrap_lons(self, element, ranges):
"""
Check whether the lons should be transformed from 0, 360 to -180, 180
"""
if isinstance(self.geographic, _CylindricalProjection):
x1, x2 = element.range(0)
self._unwrap_lons = 0 <= x1 <= 360 and 0 <= x2 <= 360
xdim = element.get_dimension(0)
x_range = ranges.get(xdim.name, {}).get('data')
if x_range:
x0, x1 = x_range
else:
x0, x1 = element.range(0)
# x0, depending on the step/interval, can be slightly less than 0,
# e.g. lon=np.arange(0, 360, 10) -> x0 = -5 from (step 10 / 2)
# other projections likely will not fall within this range
self._unwrap_lons = -90 <= x0 <= 360 and 180 <= x1 <= 540

def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
fig = super().initialize_plot(ranges, plot, plots, **opts)
style_element = self.current_frame.last if self.batched else self.current_frame
el_ranges = match_spec(style_element, self.current_ranges) if self.current_ranges else {}
if self.geographic and self.show_bounds and not self.overlaid:
from . import GeoShapePlot
shape = Shape(self.projection.boundary, crs=self.projection).options(fill_alpha=0)
shapeplot = GeoShapePlot(shape, projection=self.projection,
overlaid=True, renderer=self.renderer)
shapeplot.geographic = False
shapeplot.initialize_plot(plot=fig)
self._set_unwrap_lons(self.current_frame)
self._set_unwrap_lons(style_element, el_ranges)
return fig

def update_frame(self, key, ranges=None, element=None):
if element is not None:
self._set_unwrap_lons(element)
super().update_frame(key, ranges=ranges, element=element)
style_element = self.current_frame.last if self.batched else self.current_frame
el_ranges = match_spec(style_element, self.current_ranges) if self.current_ranges else {}
self._set_unwrap_lons(style_element, el_ranges)

def _postprocess_hover(self, renderer, source):
super()._postprocess_hover(renderer, source)
Expand Down
7 changes: 7 additions & 0 deletions geoviews/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from contextlib import suppress

from holoviews.tests.conftest import ( # noqa: F401
bokeh_backend,
port,
serve_hv,
server_cleanup,
)

import geoviews as gv

CUSTOM_MARKS = ("ui",)
Expand Down
Empty file added geoviews/tests/ui/__init__.py
Empty file.
7 changes: 0 additions & 7 deletions geoviews/tests/ui/test_example.py

This file was deleted.

81 changes: 81 additions & 0 deletions geoviews/tests/ui/test_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import cartopy.crs as ccrs
import holoviews as hv
import numpy as np
import pytest
from holoviews.tests.ui import expect, wait_until

import geoviews as gv

pytestmark = pytest.mark.ui

xr = pytest.importorskip("xarray")


@pytest.mark.usefixtures("bokeh_backend")
def test_range_correct_longitude(serve_hv):
"""
Regression test for https://github.com/holoviz/geoviews/issues/753
"""
coastline = gv.feature.coastline().opts(active_tools=["box_zoom"])
xy_range = hv.streams.RangeXY(source=coastline)

page = serve_hv(coastline)
hv_plot = page.locator(".bk-events")

expect(hv_plot).to_have_count(1)

bbox = hv_plot.bounding_box()
hv_plot.click()

page.mouse.move(bbox["x"] + 100, bbox["y"] + 100)
page.mouse.down()
page.mouse.move(bbox["x"] + 150, bbox["y"] + 150, steps=5)
page.mouse.up()

wait_until(lambda: np.isclose(xy_range.x_range[0], -105.68691588784145), page)
wait_until(lambda: np.isclose(xy_range.x_range[1], -21.80841121496224), page)


@pytest.mark.usefixtures("bokeh_backend")
@pytest.mark.parametrize("lon_start,lon_end", [(-180, 180), (0, 360)])
@pytest.mark.parametrize("bbox_x", [100, 250])
def test_rasterize_with_coastline_not_blank_on_zoom(serve_hv, lon_start, lon_end, bbox_x):
"""
Regression test for https://github.com/holoviz/geoviews/issues/726
"""
from holoviews.operation.datashader import rasterize

lon = np.linspace(lon_start, lon_end, 360)
lat = np.linspace(-90, 90, 180)
data = np.random.rand(180, 360)
ds = xr.Dataset({"data": (["lat", "lon"], data)}, coords={"lon": lon, "lat": lat})

overlay = rasterize(
gv.Image(ds, ["lon", "lat"], ["data"], crs=ccrs.PlateCarree()).opts(
tools=["hover"], active_tools=["box_zoom"]
)
) * gv.feature.coastline()

page = serve_hv(overlay)

hv_plot = page.locator(".bk-events")

expect(hv_plot).to_have_count(1)

bbox = hv_plot.bounding_box()
hv_plot.click()

page.mouse.move(bbox["x"] + bbox_x, bbox["y"] + 100)
page.mouse.down()
page.mouse.move(bbox["x"] + bbox_x + 50, bbox["y"] + 150, steps=5)
page.mouse.up()

# get hover tooltip
page.mouse.move(bbox["x"] + 100, bbox["y"] + 150)

wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page)

expect(page.locator(".bk-Tooltip")).to_contain_text("lon:")
expect(page.locator(".bk-Tooltip")).to_contain_text("lat:")
expect(page.locator(".bk-Tooltip")).to_contain_text("data:")
expect(page.locator(".bk-Tooltip")).not_to_contain_text("?")
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test-310 = ["py310", "test-core", "test-unit-task", "test", "example", "test-exa
test-311 = ["py311", "test-core", "test-unit-task", "test", "example", "test-example", "download-data"]
test-312 = ["py312", "test-core", "test-unit-task", "test", "example", "test-example", "download-data"]
test-core = ["py312", "test-unit-task", "test-core"]
test-ui = ["py312", "test-core", "test", "test-ui"]
test-ui = ["py312", "test-core", "test", "test-ui", "download-data"]
docs = ["py311", "example", "doc", "download-data"]
build = ["py311", "build"]
lint = ["py311", "lint"]
Expand Down
5 changes: 5 additions & 0 deletions scripts/download_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@

xr.tutorial.open_dataset("air_temperature")
xr.tutorial.open_dataset("rasm")

with suppress(ImportError):
from cartopy.feature import shapereader

shapereader.natural_earth()

0 comments on commit e84bf81

Please sign in to comment.