From 67687d8fe0b67ee0ffa563829f363fde79183719 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 19 Jul 2024 21:45:02 -0700 Subject: [PATCH 1/9] Automatically convert lonlat to xy --- hvplot/converter.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 25c624b6f..189912a09 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -46,7 +46,7 @@ from holoviews.plotting.util import process_cmap from holoviews.operation import histogram, apply_when from holoviews.streams import Buffer, Pipe -from holoviews.util.transform import dim +from holoviews.util.transform import dim, lon_lat_to_easting_northing from pandas import DatetimeIndex, MultiIndex from .backend_transforms import _transfer_opts_cur_backend @@ -587,6 +587,21 @@ def __init__( autorange=None, **kwds, ): + if geo and tiles and not crs and not projection: + # tiles without requiring geoviews/cartopy + # check if between -180 and 360 and lat between -90 and 90 + min_x = np.min(data[x]) + max_x = np.max(data[x]) + min_y = np.min(data[y]) + max_y = np.max(data[y]) + if -180 < min_x < 360 and -180 < max_x < 360 and -90 < min_y < 90 and -90 < max_y < 90: + data = data.copy() + lons, lats = data[x], data[y] + easting, northing = lon_lat_to_easting_northing(lons, lats) + data[x] = easting + data[y] = northing + geo = False + # Process data and related options self._redim = fields self.use_index = use_index @@ -617,8 +632,6 @@ def __init__( self.dynamic = dynamic self.geo = any([geo, crs, global_extent, projection, project, coastline, features]) - self.crs = self._process_crs(data, crs) if self.geo else None - self.output_projection = self.crs self.project = project self.coastline = coastline self.features = features @@ -627,6 +640,8 @@ def __init__( self.sort_date = sort_date # Import geoviews if geo-features requested + self.crs = self._process_crs(data, crs) if self.geo else None + self.output_projection = self.crs if self.geo or self.datatype == 'geopandas': try: import geoviews # noqa @@ -2646,7 +2661,7 @@ def image(self, x=None, y=None, z=None, data=None): redim = self._merge_redim({z[0]: self._dim_ranges['c']}) element = self._get_element('image') - if self.geo: + if self.geo and self.crs: params['crs'] = self.crs return ( element(data, [x, y], z, **params) From aa7bc1fa258d779aeda9fea3c81e2da5d51fbc02 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 19 Jul 2024 21:45:36 -0700 Subject: [PATCH 2/9] revert line change --- hvplot/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 189912a09..d658bb476 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -2661,7 +2661,7 @@ def image(self, x=None, y=None, z=None, data=None): redim = self._merge_redim({z[0]: self._dim_ranges['c']}) element = self._get_element('image') - if self.geo and self.crs: + if self.geo: params['crs'] = self.crs return ( element(data, [x, y], z, **params) From d3754a9475bb9c44cf90450baa34d302432a40dd Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 19 Jul 2024 21:47:11 -0700 Subject: [PATCH 3/9] revert line change --- hvplot/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index d658bb476..2eba08ce7 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -632,6 +632,8 @@ def __init__( self.dynamic = dynamic self.geo = any([geo, crs, global_extent, projection, project, coastline, features]) + self.crs = self._process_crs(data, crs) if self.geo else None + self.output_projection = self.crs self.project = project self.coastline = coastline self.features = features @@ -640,8 +642,6 @@ def __init__( self.sort_date = sort_date # Import geoviews if geo-features requested - self.crs = self._process_crs(data, crs) if self.geo else None - self.output_projection = self.crs if self.geo or self.datatype == 'geopandas': try: import geoviews # noqa From 2e320e0901cd6cf982a553d2de4eba7e1d4e940d Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 19 Jul 2024 21:49:59 -0700 Subject: [PATCH 4/9] Move within if --- hvplot/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 2eba08ce7..d87df07c8 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -600,7 +600,7 @@ def __init__( easting, northing = lon_lat_to_easting_northing(lons, lats) data[x] = easting data[y] = northing - geo = False + geo = False # Process data and related options self._redim = fields From d2c6ce424d1bae2de4e65030317d51556cbd6c14 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 11:17:18 -0700 Subject: [PATCH 5/9] migrate into gridded --- hvplot/converter.py | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index d87df07c8..12806d15b 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -587,21 +587,6 @@ def __init__( autorange=None, **kwds, ): - if geo and tiles and not crs and not projection: - # tiles without requiring geoviews/cartopy - # check if between -180 and 360 and lat between -90 and 90 - min_x = np.min(data[x]) - max_x = np.max(data[x]) - min_y = np.min(data[y]) - max_y = np.max(data[y]) - if -180 < min_x < 360 and -180 < max_x < 360 and -90 < min_y < 90 and -90 < max_y < 90: - data = data.copy() - lons, lats = data[x], data[y] - easting, northing = lon_lat_to_easting_northing(lons, lats) - data[x] = easting - data[y] = northing - geo = False - # Process data and related options self._redim = fields self.use_index = use_index @@ -682,6 +667,9 @@ def __init__( xlim = (x0, x1) if ylim: ylim = (y0, y1) + elif projection is False: + # to disable automatic projection of tiles + self.output_projection = projection # Operations if resample_when is not None and not any([rasterize, datashade, downsample]): @@ -2637,6 +2625,33 @@ def _process_gridded_args(self, data, x, y, z): not_found = [dim for dim in dimensions if dim not in self.variables] _, data = process_derived_datetime_pandas(data, not_found, self.indexes) + print(self.tiles, self.output_projection) + if self.tiles and self.output_projection is not False: + # tiles without requiring geoviews/cartopy + # check if between -180 and 360 and lat between -90 and 90 + _hover_code = """ + const projections = Bokeh.require("core/util/projections"); + const {snap_x, snap_y} = special_vars + const coords = projections.wgs84_mercator.invert(snap_x, snap_y) + return "" + (coords[%d]).toFixed(4) + """ + min_x = np.min(data[self.x]) + max_x = np.max(data[self.x]) + min_y = np.min(data[self.y]) + max_y = np.max(data[self.y]) + if -180 < min_x < 360 and -180 < max_x < 360 and -90 < min_y < 90 and -90 < max_y < 90: + data = data.copy() + lons, lats = data[x], data[y] + lons = (lons + 180) % 360 - 180 # ticks are better with -180 to 180 + easting, northing = lon_lat_to_easting_northing(lons, lats) + x, y = 'x', 'y' + if x in data: + x = x + '_' + if y in data: + y = y + '_' + data[x] = easting + data[y] = northing + data = data.swap_dims({self.x: x, self.y: y}) return data, x, y, z def _get_element(self, kind): From 251ddc71b9b7233cab2fdc75f96813a4b994e794 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 11:51:34 -0700 Subject: [PATCH 6/9] move and add gdf support --- hvplot/converter.py | 62 +++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 12806d15b..647f6176a 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -2095,6 +2095,40 @@ def _process_chart_args(self, data, x, y, single_y=False, categories=None): not_found = [dim for dim in dimensions if dim not in self.variables] _, data = process_derived_datetime_pandas(data, not_found, self.indexes) + data, x, y = self._process_tiles_without_geo(data, x, y) + return data, x, y + + def _process_tiles_without_geo(self, data, x, y): + """ + Tiles without requiring geoviews/cartopy. + """ + data_is_gdf = is_geodataframe(data) + if not self.tiles or self.output_projection is False: + return data, x, y + elif not data_is_gdf and (x is None or y is None): + return data, x, y + + if data_is_gdf: + data = data.to_crs(epsg=3857) + return data, x, y + else: + min_x = np.min(data[x]) + max_x = np.max(data[x]) + min_y = np.min(data[y]) + max_y = np.max(data[y]) + x_within_bounds = -180 < min_x < 360 and -180 < max_x < 360 + y_within_bounds = -90 < min_y < 90 and -90 < max_y < 90 + if x_within_bounds and y_within_bounds: + data = data.copy() + lons, lats = data[x], data[y] + lons = (lons + 180) % 360 - 180 # ticks are better with -180 to 180 + easting, northing = lon_lat_to_easting_northing(lons, lats) + new_x = 'x' if 'x' not in data else 'x_' + new_y = 'y' if 'y' not in data else 'y_' + data[new_x] = easting + data[new_y] = northing + data = data.swap_dims({x: new_x, y: new_y}) + return data, new_x, new_y return data, x, y def chart(self, element, x, y, data=None): @@ -2625,33 +2659,7 @@ def _process_gridded_args(self, data, x, y, z): not_found = [dim for dim in dimensions if dim not in self.variables] _, data = process_derived_datetime_pandas(data, not_found, self.indexes) - print(self.tiles, self.output_projection) - if self.tiles and self.output_projection is not False: - # tiles without requiring geoviews/cartopy - # check if between -180 and 360 and lat between -90 and 90 - _hover_code = """ - const projections = Bokeh.require("core/util/projections"); - const {snap_x, snap_y} = special_vars - const coords = projections.wgs84_mercator.invert(snap_x, snap_y) - return "" + (coords[%d]).toFixed(4) - """ - min_x = np.min(data[self.x]) - max_x = np.max(data[self.x]) - min_y = np.min(data[self.y]) - max_y = np.max(data[self.y]) - if -180 < min_x < 360 and -180 < max_x < 360 and -90 < min_y < 90 and -90 < max_y < 90: - data = data.copy() - lons, lats = data[x], data[y] - lons = (lons + 180) % 360 - 180 # ticks are better with -180 to 180 - easting, northing = lon_lat_to_easting_northing(lons, lats) - x, y = 'x', 'y' - if x in data: - x = x + '_' - if y in data: - y = y + '_' - data[x] = easting - data[y] = northing - data = data.swap_dims({self.x: x, self.y: y}) + data, x, y = self._process_tiles_without_geo(data, x, y) return data, x, y, z def _get_element(self, kind): From 410e91b73a67eacee1fd722624d2e489de0cdaf0 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 11:57:43 -0700 Subject: [PATCH 7/9] add check for xarray --- hvplot/converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 647f6176a..6a726b823 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -2127,7 +2127,8 @@ def _process_tiles_without_geo(self, data, x, y): new_y = 'y' if 'y' not in data else 'y_' data[new_x] = easting data[new_y] = northing - data = data.swap_dims({x: new_x, y: new_y}) + if is_xarray(data): + data = data.swap_dims({x: new_x, y: new_y}) return data, new_x, new_y return data, x, y From e1aebd5cafb70fc1d8ff19a7acf453a935789129 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 12:39:10 -0700 Subject: [PATCH 8/9] Add tests and docs --- doc/user_guide/Geographic_Data.ipynb | 30 +++++++++++++++------------- hvplot/converter.py | 24 +++++++++++----------- hvplot/tests/testgeo.py | 9 +++++++++ hvplot/tests/testgeowithoutgv.py | 8 ++++++++ hvplot/tests/testgridplots.py | 7 +++++++ 5 files changed, 52 insertions(+), 26 deletions(-) diff --git a/doc/user_guide/Geographic_Data.ipynb b/doc/user_guide/Geographic_Data.ipynb index 2f2f378a1..7a3a8c70a 100644 --- a/doc/user_guide/Geographic_Data.ipynb +++ b/doc/user_guide/Geographic_Data.ipynb @@ -60,7 +60,7 @@ "source": [ "## Tiled web map\n", "\n", - "A [tiled web map](https://en.wikipedia.org/wiki/Tiled_web_map), or tile map, is an interactive map displayed on a web browser that is divided into small, pre-rendered image tiles, allowing for efficient loading and seamless navigation across various zoom levels and geographic areas. hvPlot allows to add a tile map as a basemap to a plot with the `tiles` parameter. Importantly, `tiles` is a parameter that can be used **without installing GeoViews if the data is already projected in Web Mercator**.\n", + "A [tiled web map](https://en.wikipedia.org/wiki/Tiled_web_map), or tile map, is an interactive map displayed on a web browser that is divided into small, pre-rendered image tiles, allowing for efficient loading and seamless navigation across various zoom levels and geographic areas. hvPlot allows to add a tile map as a basemap to a plot with the `tiles` parameter. Importantly, `tiles` is a parameter that can be used **without installing GeoViews**.\n", "\n", "We'll display this dataframe of all US airports (including military bases overseas), the points are expressed in latitude/longitude coordinates:" ] @@ -78,7 +78,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We'll first start by displaying the airports **without GeoViews**. To do so, we must convert the coordinates to Web Mercator, which can be easily achieved using the `lon_lat_to_easting_northing` function provided by HoloViews, that doesn't require installing any of the usual geo dependencies like `pyproj` or `cartopy`." + "We'll first start by displaying the airports **without GeoViews** with tiles by setting `tiles=True`. \n", + "\n", + "Under the hood, hvPlot projects lat/lon to easting/northing (EPSG:4326 to EPSG:3857) coordinates without additional package dependencies if it detects that the values falls within expected lat/lon ranges.\n", + "\n", + "Note, **this feature is only available after `hvplot>0.10.0`**; older versions, `hvplot<=0.10.0`, require manual projection (see below)." ] }, { @@ -87,17 +91,14 @@ "metadata": {}, "outputs": [], "source": [ - "from holoviews.util.transform import lon_lat_to_easting_northing\n", - "\n", - "airports['x'], airports['y'] = lon_lat_to_easting_northing(airports.Longitude, airports.Latitude)\n", - "airports[['Latitude', 'Longitude', 'x', 'y']].head(3)" + "airports.hvplot.points('Longitude', 'Latitude', tiles=True, color='red', alpha=0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can now easily display the airports on a basemap by setting `tiles` to `True`." + "If you do not want it to be auto-projected under the hood for any reason, set `projection=False`, **but note it will not be properly placed on the tiles** without manual intervention. In doing so, the data will be plotted around the [null island location](https://en.wikipedia.org/wiki/Null_Island), roughly 600km off the coast of West Africa. `padding` was added to demonstrate this." ] }, { @@ -106,14 +107,14 @@ "metadata": {}, "outputs": [], "source": [ - "airports.hvplot.points('x', 'y', tiles=True, color='red', alpha=0.2)" + "airports.hvplot.points('Longitude', 'Latitude', tiles=True, projection=False, color='red', alpha=0.2, padding=10000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "A common mistake is to display data referenced in WGS84 on a tile basemap without projecting the data to Web Mercator. In doing so, the data will be plotted around the [null island location](https://en.wikipedia.org/wiki/Null_Island), roughly 600km off the coast of West Africa." + "To manually project use the `lon_lat_to_easting_northing` function provided by HoloViews, that doesn't require installing any of the usual geo dependencies like `pyproj` or `cartopy`." ] }, { @@ -122,10 +123,11 @@ "metadata": {}, "outputs": [], "source": [ - "airports.hvplot.points(\n", - " 'Longitude', 'Latitude', tiles=True, color='red', alpha=0.2,\n", - " xlim=(-1000000, 1000000), ylim=(-1000000, 1000000)\n", - ")" + "from holoviews.util.transform import lon_lat_to_easting_northing\n", + "\n", + "airports['x'], airports['y'] = lon_lat_to_easting_northing(airports.Longitude, airports.Latitude)\n", + "airports[['Latitude', 'Longitude', 'x', 'y']].head(3)\n", + "airports.hvplot.points('x', 'y', tiles=True, color='red', alpha=0.2)" ] }, { @@ -415,7 +417,7 @@ "- `global_extent` (default=False): Whether to expand the plot extent to span the whole globe\n", "- `project` (default=False): Whether to project the data before plotting (adds initial overhead but avoids projecting data when plot is dynamically updated)\n", "- `projection` (default=None): Coordinate reference system of the plot (output projection) specified as a string or integer EPSG code, a CRS or Proj pyproj object, a Cartopy CRS object or class name, a WKT string, or a proj.4 string. Defaults to PlateCarree.\n", - "- `tiles` (default=False): Whether to overlay the plot on a tile source. Accept the following values:\n", + "- `tiles` (default=False): Whether to overlay the plot on a tile source. If coordinate values fall within lat/lon bounds, auto-projects to EPSG:3857 unless `projection=False`. Accepts the following values:\n", " - `True`: OpenStreetMap layer\n", " - `xyzservices.TileProvider` instance (requires [`xyzservices`](https://xyzservices.readthedocs.io/) to be installed)\n", " - a map string name based on one of the default layers made available by [HoloViews](https://holoviews.org/reference/elements/bokeh/Tiles.html) ('CartoDark', 'CartoLight', 'EsriImagery', 'EsriNatGeo', 'EsriUSATopo', 'EsriTerrain', 'EsriStreet', 'EsriReference', 'OSM', 'OpenTopoMap') or [GeoViews](https://geoviews.org/user_guide/Working_with_Bokeh.html) ('CartoDark', 'CartoEco', 'CartoLight', 'CartoMidnight', 'EsriImagery', 'EsriNatGeo', 'EsriUSATopo', 'EsriTerrain', 'EsriReference', 'EsriOceanBase', 'EsriOceanReference', 'EsriWorldPhysical', 'EsriWorldShadedRelief', 'EsriWorldTopo', 'EsriWorldDarkGrayBase', 'EsriWorldDarkGrayReference', 'EsriWorldLightGrayBase', 'EsriWorldLightGrayReference', 'EsriWorldHillshadeDark', 'EsriWorldHillshade', 'EsriAntarcticImagery', 'EsriArcticImagery', 'EsriArcticOceanBase', 'EsriArcticOceanReference', 'EsriWorldBoundariesAndPlaces', 'EsriWorldBoundariesAndPlacesAlternate', 'EsriWorldTransportation', 'EsriDelormeWorldBaseMap', 'EsriWorldNavigationCharts', 'EsriWorldStreetMap', 'OSM', 'OpenTopoMap'). Note that Stamen tile sources require a Stadia account when not running locally; see [stadiamaps.com](https://stadiamaps.com/).\n", diff --git a/hvplot/converter.py b/hvplot/converter.py index 6a726b823..07b1785cf 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -309,7 +309,8 @@ class HoloViewsConverter: CRS object or class name, a WKT string, or a proj.4 string. Defaults to PlateCarree. tiles (default=False): - Whether to overlay the plot on a tile source: + Whether to overlay the plot on a tile source. If coordinate values fall within + lat/lon bounds, auto-projects to EPSG:3857, unless `projection=False`. - `True`: OpenStreetMap layer - `xyzservices.TileProvider` instance (requires xyzservices to be installed) @@ -2102,28 +2103,27 @@ def _process_tiles_without_geo(self, data, x, y): """ Tiles without requiring geoviews/cartopy. """ - data_is_gdf = is_geodataframe(data) - if not self.tiles or self.output_projection is False: + if self.geo or not self.tiles or self.output_projection is False: return data, x, y - elif not data_is_gdf and (x is None or y is None): + elif not is_geodataframe(data) and (x is None or y is None): return data, x, y - if data_is_gdf: - data = data.to_crs(epsg=3857) + if is_geodataframe(data): + if data.crs is not None: + data = data.to_crs(epsg=3857) return data, x, y else: min_x = np.min(data[x]) max_x = np.max(data[x]) min_y = np.min(data[y]) max_y = np.max(data[y]) - x_within_bounds = -180 < min_x < 360 and -180 < max_x < 360 - y_within_bounds = -90 < min_y < 90 and -90 < max_y < 90 + x_within_bounds = -180 <= min_x <= 360 and -180 <= max_x <= 360 + y_within_bounds = -90 <= min_y <= 90 and -90 <= max_y <= 90 if x_within_bounds and y_within_bounds: data = data.copy() - lons, lats = data[x], data[y] - lons = (lons + 180) % 360 - 180 # ticks are better with -180 to 180 - easting, northing = lon_lat_to_easting_northing(lons, lats) - new_x = 'x' if 'x' not in data else 'x_' + lons_180 = (data[x] + 180) % 360 - 180 # ticks are better with -180 to 180 + easting, northing = lon_lat_to_easting_northing(lons_180, data[y]) + new_x = 'x' if 'x' not in data else 'x_' # quick existing var check new_y = 'y' if 'y' not in data else 'y_' data[new_x] = easting data[new_y] = northing diff --git a/hvplot/tests/testgeo.py b/hvplot/tests/testgeo.py index 0ed617fa1..24930aad5 100644 --- a/hvplot/tests/testgeo.py +++ b/hvplot/tests/testgeo.py @@ -443,6 +443,15 @@ def test_geometry_none(self): polygons.geometry[1] = None assert polygons.hvplot(geo=True) + def test_tiles_without_gv(self): + polygons = self.polygons.copy() + polygons_plot = polygons.hvplot(tiles=True) + polygons_plot.get(1).data.crs is None + + polygons.crs = 'EPSG:4326' + polygons_plot = self.polygons.hvplot(tiles=True) + polygons_plot.get(1).data.crs == 'EPSG:3857' + class TestGeoUtil(TestCase): def setUp(self): diff --git a/hvplot/tests/testgeowithoutgv.py b/hvplot/tests/testgeowithoutgv.py index 97430fd96..de1d90159 100644 --- a/hvplot/tests/testgeowithoutgv.py +++ b/hvplot/tests/testgeowithoutgv.py @@ -23,6 +23,8 @@ def test_plot_tiles_doesnt_set_geo(self, simple_df): assert len(plot) == 2 assert isinstance(plot.get(0), hv.Tiles) assert 'openstreetmap' in plot.get(0).data + assert 'x_' in plot.get(1).data + assert 'y_' in plot.get(1).data bk_plot = bk_renderer.get_plot(plot) assert bk_plot.projection == 'mercator' @@ -31,6 +33,8 @@ def test_plot_specific_tiles_doesnt_set_geo(self, simple_df): assert len(plot) == 2 assert isinstance(plot.get(0), hv.Tiles) assert 'ArcGIS' in plot.get(0).data + assert 'x_' in plot.get(1).data + assert 'y_' in plot.get(1).data bk_plot = bk_renderer.get_plot(plot) assert bk_plot.projection == 'mercator' @@ -47,12 +51,16 @@ def test_plot_with_specific_tile_obj(self, simple_df): assert len(plot) == 2 assert isinstance(plot.get(0), hv.Tiles) assert 'ArcGIS' in plot.get(0).data + assert 'x_' in plot.get(1).data + assert 'y_' in plot.get(1).data bk_plot = bk_renderer.get_plot(plot) assert bk_plot.projection == 'mercator' def test_plot_with_xyzservices_tileprovider(self, simple_df): xyzservices = pytest.importorskip('xyzservices') plot = simple_df.hvplot.points('x', 'y', tiles=xyzservices.providers.Esri.WorldImagery) + assert 'x_' in plot.get(1).data + assert 'y_' in plot.get(1).data assert len(plot) == 2 assert isinstance(plot.get(0), hv.Tiles) assert isinstance(plot.get(0).data, xyzservices.TileProvider) diff --git a/hvplot/tests/testgridplots.py b/hvplot/tests/testgridplots.py index 1a90b1ed8..2ab063eb5 100644 --- a/hvplot/tests/testgridplots.py +++ b/hvplot/tests/testgridplots.py @@ -257,3 +257,10 @@ def test_dataarray_label_precedence(self): plot = self.da_rgb.sel(band=1).hvplot.image(label='b', value_label='c') assert plot.vdims[0].name == 'b' + + def test_tiles_without_gv(self): + plot = self.ds.hvplot('lon', 'lat', tiles=True) + assert len(plot) == 2 + assert isinstance(plot.get(1), Image) + assert 'x' in plot.get(1).data + assert 'y' in plot.get(1).data From 732389ee6568a6e6d69eb6c7ffb65616b69163ac Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 12:52:42 -0700 Subject: [PATCH 9/9] mention tiles --- doc/user_guide/Customization.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_guide/Customization.ipynb b/doc/user_guide/Customization.ipynb index 4f99b0b6f..8a097c19c 100644 --- a/doc/user_guide/Customization.ipynb +++ b/doc/user_guide/Customization.ipynb @@ -229,7 +229,7 @@ " projection (default=None):\n", " Coordinate reference system of the plot (output projection) specified as a string or integer EPSG code, a CRS or Proj pyproj object, a Cartopy CRS object or class name, a WKT string, or a proj.4 string. Defaults to PlateCarree.\n", " tiles (default=False):\n", - " Whether to overlay the plot on a tile source. Tiles sources\n", + " Whether to overlay the plot on a tile source. If coordinate values fall within lat/lon bounds, auto-projects to EPSG:3857 unless `projection=False`. Tiles sources\n", " can be selected by name or a tiles object or class can be passed,\n", " the default is 'Wikipedia'.\n", " tiles_opts (default=None): dict\n",