From 6288baa052690eb0d9ce6ccfd50b4e9d9dd5421b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 11:43:02 -0800 Subject: [PATCH 01/10] Expose windbarbs --- doc/conf.py | 1 + doc/gallery/gridded/barbs.ipynb | 79 ++++++++++ doc/ref/api/index.md | 2 + doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb | 158 +++++++++++++++++++ hvplot/converter.py | 34 +++- hvplot/plotting/core.py | 45 ++++++ hvplot/tests/testgeo.py | 44 ++++++ hvplot/ui.py | 2 +- 8 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 doc/gallery/gridded/barbs.ipynb create mode 100644 doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb diff --git a/doc/conf.py b/doc/conf.py index e1b0bc8b1..74306de5e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -233,6 +233,7 @@ 'reference/xarray/line': 'ref/api/manual/hvplot.hvPlot.line', 'reference/xarray/quadmesh': 'ref/api/manual/hvplot.hvPlot.quadmesh', 'reference/xarray/rgb': 'ref/api/manual/hvplot.hvPlot.rgb', + 'reference/xarray/barbs': 'ref/api/manual/hvplot.hvPlot.barbs', 'reference/xarray/vectorfield': 'ref/api/manual/hvplot.hvPlot.vectorfield', 'reference/xarray/violin': 'ref/api/manual/hvplot.hvPlot.violin', # When the pandas section was renamed tabular: diff --git a/doc/gallery/gridded/barbs.ipynb b/doc/gallery/gridded/barbs.ipynb new file mode 100644 index 000000000..bd1c365d7 --- /dev/null +++ b/doc/gallery/gridded/barbs.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c73d803c-a005-496f-9812-7eb414ce6449", + "metadata": {}, + "source": [ + "# Wind Barbs\n", + "\n", + "A wind barbs plot showing wind speed and direction using meteorological wind barb symbols.\n", + "\n", + ":::{note}\n", + "Wind barb plots require `geoviews` to be installed.\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac5ea091-6484-4fc8-9852-d48acd90cc74", + "metadata": {}, + "outputs": [], + "source": [ + "import hvplot.xarray # noqa\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "def sample_wind_data(shape=(20, 30)):\n", + " x = np.linspace(311.9, 391.1, shape[1])\n", + " y = np.linspace(-23.6, 24.8, shape[0])\n", + " x2d, y2d = np.meshgrid(x, y)\n", + " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", + " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", + " return x, y, u, v\n", + "\n", + "xs, ys, U, V = sample_wind_data()\n", + "# Calculate magnitude and angle for wind barbs\n", + "mag = np.sqrt(U**2 + V**2)\n", + "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "\n", + "ds = xr.Dataset({\n", + " 'speed': xr.DataArray(mag, dims=('y', 'x'), coords={'y': ys, 'x': xs}),\n", + " 'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n", + "})\n", + "\n", + "ds.hvplot.barbs(\n", + " x='x',\n", + " y='y',\n", + " angle='angle',\n", + " mag='speed',\n", + " color='speed',\n", + " cmap='viridis',\n", + " colorbar=True,\n", + " title='Wind Barbs Plot',\n", + " width=700,\n", + " height=400\n", + ").opts(magnitude='speed')" + ] + }, + { + "cell_type": "markdown", + "id": "4f0cd955-3e03-42a5-b581-f828036e1716", + "metadata": {}, + "source": [ + ":::{seealso}\n", + "- [Wind Barbs reference documentation](../../ref/api/manual/hvplot.hvPlot.barbs.ipynb).\n", + ":::" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/ref/api/index.md b/doc/ref/api/index.md index 2d4e91ee5..f5a759034 100644 --- a/doc/ref/api/index.md +++ b/doc/ref/api/index.md @@ -64,6 +64,7 @@ This section documents all the plotting methods of the `hvPlot` class, which as hvPlot.area hvPlot.bar + hvPlot.barbs hvPlot.barh hvPlot.box hvPlot.bivariate @@ -123,6 +124,7 @@ hvPlot's structure is based on Pandas' plotting API and as such provides special hvplot.hvPlot.area hvplot.hvPlot.bar +hvplot.hvPlot.barbs hvplot.hvPlot.barh hvplot.hvPlot.box hvplot.hvPlot.bivariate diff --git a/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb new file mode 100644 index 000000000..e15d6ba99 --- /dev/null +++ b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# hvPlot.barbs\n", + "\n", + "```{eval-rst}\n", + ".. currentmodule:: hvplot\n", + "\n", + ".. automethod:: hvPlot.barbs\n", + "```\n", + "\n", + ":::{note}\n", + "Wind barb plots require `geoviews` to be installed.\n", + ":::\n", + "\n", + "## Backend-specific styling options\n", + "\n", + "```{eval-rst}\n", + ".. backend-styling-options:: barbs\n", + "```\n", + "\n", + "## Examples\n", + "\n", + "### Basic wind barbs plot\n", + "\n", + "In this example we create a simple DataFrame with 4 columns `x`, `y`, `angle` and `speed`. `x` and `y` represent the location of the wind barbs. `angle` defines the wind direction expressed in radians (meteorological convention: direction FROM which the wind is blowing, with 0 being North). `speed` defines the wind speed magnitude." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import hvplot.pandas\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "df = pd.DataFrame(dict(\n", + " x=[-1, 0, 1, 0],\n", + " y=[0, -1, 0, 1],\n", + " angle=[0, np.pi/2, np.pi, 3*np.pi/2],\n", + " speed=[5, 10, 15, 20],\n", + "))\n", + "\n", + "df.hvplot.barbs(\n", + " x=\"x\", y=\"y\", angle=\"angle\", mag=\"speed\",\n", + " data_aspect=1, padding=0.4, width=400, height=400,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Xarray example\n", + "\n", + "In this example we also set the [`color`](option-color) to the speed variable and add a colorbar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import hvplot.xarray # noqa\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "def sample_wind_data(shape=(20, 30)):\n", + " x = np.linspace(311.9, 391.1, shape[1])\n", + " y = np.linspace(-23.6, 24.8, shape[0])\n", + " x2d, y2d = np.meshgrid(x, y)\n", + " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", + " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", + " return x, y, u, v\n", + "\n", + "xs, ys, U, V = sample_wind_data()\n", + "mag = np.sqrt(U**2 + V**2)\n", + "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "ds = xr.Dataset({\n", + " 'speed': xr.DataArray(mag, dims=('y', 'x'), coords={'y': ys, 'x': xs}),\n", + " 'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n", + "})\n", + "\n", + "ds.hvplot.barbs(\n", + " x='x', y='y', angle='angle', mag='speed',\n", + " color='speed', cmap='viridis', colorbar=True,\n", + " width=700, height=400\n", + ").opts(scale=0.3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Geographic example with Xarray\n", + "\n", + "The `xarray.Dataset` constructed in this example has a `'crs'` key in its `attrs` dictionary, which lets us simply set `geo=True` to turn this plot into a correctly projected geographic plot overlaid on web map tiles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import cartopy.crs as ccrs\n", + "import hvplot.xarray # noqa\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "def sample_data(shape=(20, 30)):\n", + " \"\"\"\n", + " Return ``(x, y, u, v, crs)`` of some vector data computed mathematically.\n", + " The returned crs will be a rotated pole CRS, meaning that the vectors\n", + " will be unevenly spaced in regular PlateCarree space.\n", + " \"\"\"\n", + " crs = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)\n", + "\n", + " x = np.linspace(311.9, 391.1, shape[1])\n", + " y = np.linspace(-23.6, 24.8, shape[0])\n", + " x2d, y2d = np.meshgrid(x, y)\n", + " u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n", + " v = 20 * np.cos(6 * np.deg2rad(x2d))\n", + " return x, y, u, v, crs\n", + "\n", + "xs, ys, U, V, crs = sample_data()\n", + "mag = np.sqrt(U**2 + V**2)\n", + "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "ds = xr.Dataset(\n", + " {\n", + " 'speed': xr.DataArray(mag, dims=('y', 'x'), coords={'y': ys, 'x': xs}),\n", + " 'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n", + " },\n", + " attrs={'crs': crs},\n", + ")\n", + "\n", + "ds.hvplot.barbs(\n", + " x=\"x\", y=\"y\", angle=\"angle\", mag=\"speed\",\n", + " geo=True, tiles=\"CartoLight\", width=700, height=400\n", + ").opts(\"WindBarbs\", scale=0.3)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/hvplot/converter.py b/hvplot/converter.py index 35a5260fb..f79764c0d 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -90,6 +90,11 @@ ) from .utilities import hvplot_extension +try: + from geoviews import WindBarbs +except ImportError: + WindBarbs = None + renderer = hv.renderer('bokeh') @@ -574,7 +579,9 @@ class HoloViewsConverter: _geom_types = ['paths', 'polygons'] _geo_types = sorted( - _gridded_types + _geom_types + ['points', 'vectorfield', 'labels', 'hexbin', 'bivariate'] + _gridded_types + + _geom_types + + ['points', 'barbs', 'vectorfield', 'labels', 'hexbin', 'bivariate'] ) _stats_types = ['hist', 'kde', 'violin', 'box', 'density'] @@ -745,6 +752,7 @@ class HoloViewsConverter: 'area': ['x', 'y', 'y2', 'stacked'], 'bar': ['x', 'y', 'stacked'], 'barh': ['x', 'y', 'stacked'], + 'barbs': ['x', 'y', 'angle', 'mag'], 'box': ['x', 'y'], 'errorbars': ['x', 'y', 'yerr1', 'yerr2'], 'bivariate': ['x', 'y', 'bandwidth', 'cut', 'filled', 'levels'], @@ -777,6 +785,7 @@ class HoloViewsConverter: 'area': Area, 'bar': Bars, 'barh': Bars, + 'barbs': WindBarbs, 'bivariate': Bivariate, 'box': BoxWhisker, 'contour': Contours, @@ -3275,6 +3284,29 @@ def contourf(self, x=None, y=None, z=None, data=None): else: return contourf + def barbs(self, x=None, y=None, angle=None, mag=None, data=None): + self._error_if_unavailable('barbs') + data, x, y, _ = self._process_gridded_args(data, x, y, z=None) + + if not (x and y): + x, y = list(k for k, v in data.coords.items() if v.size > 1) + + angle = self.kwds.get('angle') + mag = self.kwds.get('mag') + z = [angle, mag] + self.hover_cols + redim = self._merge_redim({z[1]: self._dim_ranges['c']}) + params = dict(self._relabel) + + element = self._get_element('barbs') + cur_opts, compat_opts = self._get_compat_opts('Barbs') + if self.geo: + params['crs'] = self.crs + + return redim_( + element(data, [x, y], z, **params), + **redim, + ).apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts) + def vectorfield(self, x=None, y=None, angle=None, mag=None, data=None): self._error_if_unavailable('vectorfield') data, x, y, _ = self._process_gridded_args(data, x, y, z=None) diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index a4724469b..8c1e5942d 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -250,6 +250,7 @@ class hvPlotTabular(hvPlotBase): 'table', 'dataset', 'points', + 'barbs', 'vectorfield', 'polygons', 'paths', @@ -1312,6 +1313,49 @@ def points(self, x=None, y=None, **kwds): """ return self(x, y, kind='points', **kwds) + def barbs(self, x=None, y=None, angle=None, mag=None, **kwds): + """ + A barbs plot visualizes wind barbs given by the (``x``, ``y``) starting point, + a magnitude (``mag``) and an `angle`. + + Reference: https://hvplot.holoviz.org/ref/api/manual/hvplot.hvPlot.barbs.html + + Plotting options: https://hvplot.holoviz.org/ref/plotting_options/index.html + + Parameters + ---------- + x : string + Field name to draw x-positions from + y : string + Field name to draw y-positions from + mag : string + Magnitude. + angle : string + Angle in radians. + **kwds : optional + Additional keywords arguments are documented in :ref:`plot-options`. + Run ``hvplot.help('vectorfield')`` for the full method documentation. + + Returns + ------- + :class:`holoviews:holoviews.element.VectorField` / Panel object + You can `print` the object to study its composition and run: + + .. code-block:: + + import holoviews as hv + hv.help(the_holoviews_object) + + to learn more about its parameters and options. + + References + ---------- + + - HoloViews: https://holoviews.org/reference/elements/bokeh/Barbs.html + - Matplotlib: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barbs.html + """ + return self(x, y, angle=angle, mag=mag, kind='barbs', **kwds) + def vectorfield(self, x=None, y=None, angle=None, mag=None, **kwds): """ vectorfield visualizes vectors given by the (``x``, ``y``) starting point, @@ -1721,6 +1765,7 @@ class hvPlot(hvPlotTabular): 'table', 'dataset', 'points', + 'barbs', 'vectorfield', 'polygons', 'paths', diff --git a/hvplot/tests/testgeo.py b/hvplot/tests/testgeo.py index 15663a23c..20c669540 100644 --- a/hvplot/tests/testgeo.py +++ b/hvplot/tests/testgeo.py @@ -554,3 +554,47 @@ def test_proj_to_cartopy_nearsided_perspective(self): crs = proj_to_cartopy(proj4_string) assert isinstance(crs, self.ccrs.NearsidePerspective) + + +class TestWindBarbs(TestCase): + def setUp(self): + if sys.platform == 'win32': + raise SkipTest('Skip geo tests on windows for now') + try: + import geoviews as gv # noqa + + self.gv = gv + except ImportError: + raise SkipTest('geoviews not available') + import hvplot.pandas # noqa + + def test_barbs_with_angle_mag(self): + """Test wind barbs plot with angle and magnitude""" + df = pd.DataFrame( + { + 'lon': np.linspace(-10, 10, 20), + 'lat': np.linspace(-10, 10, 20), + 'angle': np.random.uniform(0, 2 * np.pi, 20), + 'mag': np.random.uniform(0, 10, 20), + } + ) + + plot = df.hvplot.barbs(x='lon', y='lat', angle='angle', mag='mag', geo=True) + assert isinstance(plot, self.gv.WindBarbs) + assert plot.kdims[0].name == 'lon' + assert plot.kdims[1].name == 'lat' + assert plot.vdims[0].name == 'angle' + assert plot.vdims[1].name == 'mag' + + def test_barbs_invalid_incomplete_angle_mag(self): + """Test that providing only angle or mag raises an error""" + df = pd.DataFrame( + { + 'lon': np.linspace(-10, 10, 10), + 'lat': np.linspace(-10, 10, 10), + 'angle': np.random.uniform(0, 2 * np.pi, 10), + } + ) + + with pytest.raises(ValueError, match='requires either both'): + df.hvplot.barbs(x='lon', y='lat', angle='angle') diff --git a/hvplot/ui.py b/hvplot/ui.py index b1024cf5e..02e20aafc 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -26,7 +26,7 @@ } KINDS['2d'] = ( - ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield', 'points', 'paths'] + ['bivariate', 'heatmap', 'hexbin', 'labels', 'barbs', 'vectorfield', 'points', 'paths'] + KINDS['gridded'] + KINDS['geom'] ) From 252828b809828d771507eddb6c8830d01aa213e1 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 12:33:37 -0800 Subject: [PATCH 02/10] Bug fix and tweak --- doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb | 17 ++++++++--------- hvplot/converter.py | 6 ++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb index e15d6ba99..d667e709a 100644 --- a/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb +++ b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb @@ -40,15 +40,15 @@ "import numpy as np\n", "\n", "df = pd.DataFrame(dict(\n", - " x=[-1, 0, 1, 0],\n", - " y=[0, -1, 0, 1],\n", - " angle=[0, np.pi/2, np.pi, 3*np.pi/2],\n", - " speed=[5, 10, 15, 20],\n", + " x=[0, -1, 0, 1, 0, 1, 1, -1, -1],\n", + " y=[0, 0, -1, 0, 1, 1, -1, -1, 1],\n", + " angle=[0, 0, np.pi/2, np.pi, 3*np.pi/2, np.pi/4, np.pi/3, np.pi/6, np.pi/8],\n", + " speed=[0, 5, 10, 15, 20, 50, 75, 100, 150],\n", "))\n", "\n", "df.hvplot.barbs(\n", " x=\"x\", y=\"y\", angle=\"angle\", mag=\"speed\",\n", - " data_aspect=1, padding=0.4, width=400, height=400,\n", + " data_aspect=1, padding=0.2, width=400, height=400,\n", ")" ] }, @@ -58,7 +58,7 @@ "source": [ "### Xarray example\n", "\n", - "In this example we also set the [`color`](option-color) to the speed variable and add a colorbar." + "In this example we show how to create a barb plot using Xarray data with hvPlot." ] }, { @@ -89,9 +89,8 @@ "\n", "ds.hvplot.barbs(\n", " x='x', y='y', angle='angle', mag='speed',\n", - " color='speed', cmap='viridis', colorbar=True,\n", - " width=700, height=400\n", - ").opts(scale=0.3)" + " width=700, height=400, scale=0.3\n", + ")" ] }, { diff --git a/hvplot/converter.py b/hvplot/converter.py index f79764c0d..9b7846df9 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -616,6 +616,8 @@ class HoloViewsConverter: 'tiles_opts', 'projection', 'global_extent', + 'xlim', + 'ylim', ] _size_layout_options = [ @@ -752,7 +754,7 @@ class HoloViewsConverter: 'area': ['x', 'y', 'y2', 'stacked'], 'bar': ['x', 'y', 'stacked'], 'barh': ['x', 'y', 'stacked'], - 'barbs': ['x', 'y', 'angle', 'mag'], + 'barbs': ['x', 'y', 'angle', 'mag', 'scale'], 'box': ['x', 'y'], 'errorbars': ['x', 'y', 'yerr1', 'yerr2'], 'bivariate': ['x', 'y', 'bandwidth', 'cut', 'filled', 'levels'], @@ -3298,7 +3300,7 @@ def barbs(self, x=None, y=None, angle=None, mag=None, data=None): params = dict(self._relabel) element = self._get_element('barbs') - cur_opts, compat_opts = self._get_compat_opts('Barbs') + cur_opts, compat_opts = self._get_compat_opts('WindBarbs') if self.geo: params['crs'] = self.crs From c4a1bd9c1f601caac2d6a484b7ef7c1840ac5a5e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 12:38:17 -0800 Subject: [PATCH 03/10] Add import guard --- hvplot/plotting/core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index 8c1e5942d..e614ea0c4 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -1354,6 +1354,12 @@ def barbs(self, x=None, y=None, angle=None, mag=None, **kwds): - HoloViews: https://holoviews.org/reference/elements/bokeh/Barbs.html - Matplotlib: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barbs.html """ + try: + import geoviews as gv # noqa: F401 + except ImportError: + raise ImportError( + 'geoviews is required for barbs plots. Please install geoviews to use this feature.' + ) return self(x, y, angle=angle, mag=mag, kind='barbs', **kwds) def vectorfield(self, x=None, y=None, angle=None, mag=None, **kwds): From 3d51c290f58fc9634ced69c1b704e68ecbafd3ae Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 12:40:30 -0800 Subject: [PATCH 04/10] rm unnecessary change --- hvplot/converter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 9b7846df9..657b0455e 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -656,8 +656,6 @@ class HoloViewsConverter: 'yformatter', 'xlabel', 'ylabel', - 'xlim', - 'ylim', 'xticks', 'yticks', 'cticks', From 767e06088835d771bbf0a8300d7384ac1369fd9b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 12:55:58 -0800 Subject: [PATCH 05/10] rm unnecessary change --- hvplot/converter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 657b0455e..8ced89d1d 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -616,8 +616,6 @@ class HoloViewsConverter: 'tiles_opts', 'projection', 'global_extent', - 'xlim', - 'ylim', ] _size_layout_options = [ From f0c626be1fbd3e66e4cd3008c92400e4b2e914bc Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 15:51:56 -0800 Subject: [PATCH 06/10] address review --- doc/conf.py | 1 - doc/gallery/gridded/barbs.ipynb | 2 +- doc/ref/api/index.md | 4 +-- doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb | 12 +++---- hvplot/converter.py | 33 +++++++++++--------- hvplot/plotting/core.py | 30 +++++++++--------- hvplot/tests/testgeo.py | 4 +-- hvplot/ui.py | 2 +- 8 files changed, 46 insertions(+), 42 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 74306de5e..e1b0bc8b1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -233,7 +233,6 @@ 'reference/xarray/line': 'ref/api/manual/hvplot.hvPlot.line', 'reference/xarray/quadmesh': 'ref/api/manual/hvplot.hvPlot.quadmesh', 'reference/xarray/rgb': 'ref/api/manual/hvplot.hvPlot.rgb', - 'reference/xarray/barbs': 'ref/api/manual/hvplot.hvPlot.barbs', 'reference/xarray/vectorfield': 'ref/api/manual/hvplot.hvPlot.vectorfield', 'reference/xarray/violin': 'ref/api/manual/hvplot.hvPlot.violin', # When the pandas section was renamed tabular: diff --git a/doc/gallery/gridded/barbs.ipynb b/doc/gallery/gridded/barbs.ipynb index bd1c365d7..fcc1f9a15 100644 --- a/doc/gallery/gridded/barbs.ipynb +++ b/doc/gallery/gridded/barbs.ipynb @@ -43,7 +43,7 @@ " 'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n", "})\n", "\n", - "ds.hvplot.barbs(\n", + "ds.hvplot.windbarbs(\n", " x='x',\n", " y='y',\n", " angle='angle',\n", diff --git a/doc/ref/api/index.md b/doc/ref/api/index.md index f5a759034..bcef3b678 100644 --- a/doc/ref/api/index.md +++ b/doc/ref/api/index.md @@ -64,7 +64,6 @@ This section documents all the plotting methods of the `hvPlot` class, which as hvPlot.area hvPlot.bar - hvPlot.barbs hvPlot.barh hvPlot.box hvPlot.bivariate @@ -95,6 +94,7 @@ This section documents all the plotting methods of the `hvPlot` class, which as .. autosummary:: + hvPlot.windbarbs hvPlot.contour hvPlot.contourf hvPlot.image @@ -124,7 +124,7 @@ hvPlot's structure is based on Pandas' plotting API and as such provides special hvplot.hvPlot.area hvplot.hvPlot.bar -hvplot.hvPlot.barbs +hvplot.hvPlot.windbarbs hvplot.hvPlot.barh hvplot.hvPlot.box hvplot.hvPlot.bivariate diff --git a/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb index d667e709a..0c988350f 100644 --- a/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb +++ b/doc/ref/api/manual/hvplot.hvPlot.barbs.ipynb @@ -4,12 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# hvPlot.barbs\n", + "# hvPlot.windbarbs\n", "\n", "```{eval-rst}\n", ".. currentmodule:: hvplot\n", "\n", - ".. automethod:: hvPlot.barbs\n", + ".. automethod:: hvPlot.windbarbs\n", "```\n", "\n", ":::{note}\n", @@ -19,7 +19,7 @@ "## Backend-specific styling options\n", "\n", "```{eval-rst}\n", - ".. backend-styling-options:: barbs\n", + ".. backend-styling-options:: windbarbs\n", "```\n", "\n", "## Examples\n", @@ -46,7 +46,7 @@ " speed=[0, 5, 10, 15, 20, 50, 75, 100, 150],\n", "))\n", "\n", - "df.hvplot.barbs(\n", + "df.hvplot.windbarbs(\n", " x=\"x\", y=\"y\", angle=\"angle\", mag=\"speed\",\n", " data_aspect=1, padding=0.2, width=400, height=400,\n", ")" @@ -87,7 +87,7 @@ " 'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n", "})\n", "\n", - "ds.hvplot.barbs(\n", + "ds.hvplot.windbarbs(\n", " x='x', y='y', angle='angle', mag='speed',\n", " width=700, height=400, scale=0.3\n", ")" @@ -139,7 +139,7 @@ " attrs={'crs': crs},\n", ")\n", "\n", - "ds.hvplot.barbs(\n", + "ds.hvplot.windbarbs(\n", " x=\"x\", y=\"y\", angle=\"angle\", mag=\"speed\",\n", " geo=True, tiles=\"CartoLight\", width=700, height=400\n", ").opts(\"WindBarbs\", scale=0.3)" diff --git a/hvplot/converter.py b/hvplot/converter.py index 8ced89d1d..2094d0fd9 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -90,11 +90,6 @@ ) from .utilities import hvplot_extension -try: - from geoviews import WindBarbs -except ImportError: - WindBarbs = None - renderer = hv.renderer('bokeh') @@ -581,7 +576,7 @@ class HoloViewsConverter: _geo_types = sorted( _gridded_types + _geom_types - + ['points', 'barbs', 'vectorfield', 'labels', 'hexbin', 'bivariate'] + + ['points', 'windbarbs', 'vectorfield', 'labels', 'hexbin', 'bivariate'] ) _stats_types = ['hist', 'kde', 'violin', 'box', 'density'] @@ -654,6 +649,8 @@ class HoloViewsConverter: 'yformatter', 'xlabel', 'ylabel', + 'xlim', + 'ylim', 'xticks', 'yticks', 'cticks', @@ -750,7 +747,7 @@ class HoloViewsConverter: 'area': ['x', 'y', 'y2', 'stacked'], 'bar': ['x', 'y', 'stacked'], 'barh': ['x', 'y', 'stacked'], - 'barbs': ['x', 'y', 'angle', 'mag', 'scale'], + 'windbarbs': ['x', 'y', 'angle', 'mag', 'scale'], 'box': ['x', 'y'], 'errorbars': ['x', 'y', 'yerr1', 'yerr2'], 'bivariate': ['x', 'y', 'bandwidth', 'cut', 'filled', 'levels'], @@ -783,7 +780,6 @@ class HoloViewsConverter: 'area': Area, 'bar': Bars, 'barh': Bars, - 'barbs': WindBarbs, 'bivariate': Bivariate, 'box': BoxWhisker, 'contour': Contours, @@ -970,8 +966,9 @@ def __init__( self.dynamic = dynamic self.geo = any([geo, crs, global_extent, projection, project, coastline, features]) # Try importing geoviews if geo-features requested - if self.geo or self.datatype == 'geopandas': - import_geoviews() + if self.geo or self.datatype == 'geopandas' or kind == 'windbarbs': + gv = import_geoviews() + self._kind_mapping['windbarbs'] = gv.WindBarbs self.crs = self._process_crs(data, crs) if self.geo else None self.output_projection = self.crs @@ -3282,12 +3279,15 @@ def contourf(self, x=None, y=None, z=None, data=None): else: return contourf - def barbs(self, x=None, y=None, angle=None, mag=None, data=None): - self._error_if_unavailable('barbs') + def windbarbs(self, x=None, y=None, angle=None, mag=None, data=None): + self._error_if_unavailable('windbarbs') data, x, y, _ = self._process_gridded_args(data, x, y, z=None) if not (x and y): - x, y = list(k for k, v in data.coords.items() if v.size > 1) + if hasattr(data, 'coords'): + x, y = list(k for k, v in data.coords.items() if v.size > 1) + else: + x, y = data.columns[:2] angle = self.kwds.get('angle') mag = self.kwds.get('mag') @@ -3295,7 +3295,7 @@ def barbs(self, x=None, y=None, angle=None, mag=None, data=None): redim = self._merge_redim({z[1]: self._dim_ranges['c']}) params = dict(self._relabel) - element = self._get_element('barbs') + element = self._get_element('windbarbs') cur_opts, compat_opts = self._get_compat_opts('WindBarbs') if self.geo: params['crs'] = self.crs @@ -3310,7 +3310,10 @@ def vectorfield(self, x=None, y=None, angle=None, mag=None, data=None): data, x, y, _ = self._process_gridded_args(data, x, y, z=None) if not (x and y): - x, y = list(k for k, v in data.coords.items() if v.size > 1) + if hasattr(data, 'coords'): + x, y = list(k for k, v in data.coords.items() if v.size > 1) + else: + x, y = data.columns[:2] angle = self.kwds.get('angle') mag = self.kwds.get('mag') diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index e614ea0c4..d66ebe48c 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -250,7 +250,7 @@ class hvPlotTabular(hvPlotBase): 'table', 'dataset', 'points', - 'barbs', + 'windbarbs', 'vectorfield', 'polygons', 'paths', @@ -1313,12 +1313,14 @@ def points(self, x=None, y=None, **kwds): """ return self(x, y, kind='points', **kwds) - def barbs(self, x=None, y=None, angle=None, mag=None, **kwds): + def windbarbs(self, x=None, y=None, angle=None, mag=None, **kwds): """ - A barbs plot visualizes wind barbs given by the (``x``, ``y``) starting point, - a magnitude (``mag``) and an `angle`. + A windbarbs plot visualizes wind barbs given by the (``x``, ``y``) starting point, + a magnitude (``mag``) and an ``angle``. - Reference: https://hvplot.holoviz.org/ref/api/manual/hvplot.hvPlot.barbs.html + .. versionadded:: 1.0.0 + + Reference: https://hvplot.holoviz.org/ref/api/manual/hvplot.hvPlot.windbarbs.html Plotting options: https://hvplot.holoviz.org/ref/plotting_options/index.html @@ -1334,38 +1336,38 @@ def barbs(self, x=None, y=None, angle=None, mag=None, **kwds): Angle in radians. **kwds : optional Additional keywords arguments are documented in :ref:`plot-options`. - Run ``hvplot.help('vectorfield')`` for the full method documentation. + Run ``hvplot.help('windbarbs')`` for the full method documentation. Returns ------- - :class:`holoviews:holoviews.element.VectorField` / Panel object + :class:`geoviews:geoviews.element.Barbs` / Panel object You can `print` the object to study its composition and run: .. code-block:: - import holoviews as hv - hv.help(the_holoviews_object) + import geoviews as gv + gv.help(the_geoviews_object) to learn more about its parameters and options. References ---------- - - HoloViews: https://holoviews.org/reference/elements/bokeh/Barbs.html + - GeoViews: https://geoviews.org/gallery/bokeh/wind_barbs_example.html - Matplotlib: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barbs.html """ try: import geoviews as gv # noqa: F401 except ImportError: raise ImportError( - 'geoviews is required for barbs plots. Please install geoviews to use this feature.' + 'geoviews>=1.15.0 is required for windbarbs plots. Please install geoviews to use this feature.' ) - return self(x, y, angle=angle, mag=mag, kind='barbs', **kwds) + return self(x, y, angle=angle, mag=mag, kind='windbarbs', **kwds) def vectorfield(self, x=None, y=None, angle=None, mag=None, **kwds): """ vectorfield visualizes vectors given by the (``x``, ``y``) starting point, - a magnitude (``mag``) and an `angle`. A ``vectorfield`` plot is also known + a magnitude (``mag``) and an ``angle``. A ``vectorfield`` plot is also known as a ``quiver`` plot. Reference: https://hvplot.holoviz.org/ref/api/manual/hvplot.hvPlot.vectorfield.html @@ -1771,7 +1773,7 @@ class hvPlot(hvPlotTabular): 'table', 'dataset', 'points', - 'barbs', + 'windbarbs', 'vectorfield', 'polygons', 'paths', diff --git a/hvplot/tests/testgeo.py b/hvplot/tests/testgeo.py index 20c669540..5d4d1ec11 100644 --- a/hvplot/tests/testgeo.py +++ b/hvplot/tests/testgeo.py @@ -579,7 +579,7 @@ def test_barbs_with_angle_mag(self): } ) - plot = df.hvplot.barbs(x='lon', y='lat', angle='angle', mag='mag', geo=True) + plot = df.hvplot.windbarbs(x='lon', y='lat', angle='angle', mag='mag', geo=True) assert isinstance(plot, self.gv.WindBarbs) assert plot.kdims[0].name == 'lon' assert plot.kdims[1].name == 'lat' @@ -597,4 +597,4 @@ def test_barbs_invalid_incomplete_angle_mag(self): ) with pytest.raises(ValueError, match='requires either both'): - df.hvplot.barbs(x='lon', y='lat', angle='angle') + df.hvplot.windbarbs(x='lon', y='lat', angle='angle') diff --git a/hvplot/ui.py b/hvplot/ui.py index 02e20aafc..611af0c57 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -26,7 +26,7 @@ } KINDS['2d'] = ( - ['bivariate', 'heatmap', 'hexbin', 'labels', 'barbs', 'vectorfield', 'points', 'paths'] + ['bivariate', 'heatmap', 'hexbin', 'labels', 'windbarbs', 'vectorfield', 'points', 'paths'] + KINDS['gridded'] + KINDS['geom'] ) From f31129365ea9cc3081d591160b3f0e1834b5fd2f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 16:07:30 -0800 Subject: [PATCH 07/10] fix --- doc/gallery/gridded/barbs.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gallery/gridded/barbs.ipynb b/doc/gallery/gridded/barbs.ipynb index fcc1f9a15..71a1fa809 100644 --- a/doc/gallery/gridded/barbs.ipynb +++ b/doc/gallery/gridded/barbs.ipynb @@ -54,7 +54,7 @@ " title='Wind Barbs Plot',\n", " width=700,\n", " height=400\n", - ").opts(magnitude='speed')" + ")" ] }, { From dae45b596c1f27e8e5616dac6a4664919cce8e87 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 16:09:26 -0800 Subject: [PATCH 08/10] dynamically add barbs --- hvplot/converter.py | 5 ++--- hvplot/plotting/core.py | 10 +++------- hvplot/util.py | 6 ++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 2094d0fd9..e4ddbee4b 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -966,9 +966,8 @@ def __init__( self.dynamic = dynamic self.geo = any([geo, crs, global_extent, projection, project, coastline, features]) # Try importing geoviews if geo-features requested - if self.geo or self.datatype == 'geopandas' or kind == 'windbarbs': - gv = import_geoviews() - self._kind_mapping['windbarbs'] = gv.WindBarbs + if self.geo or self.datatype == 'geopandas': + import_geoviews() self.crs = self._process_crs(data, crs) if self.geo else None self.output_projection = self.crs diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index d66ebe48c..9a6a78829 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -13,7 +13,7 @@ panel_available = False from ..converter import HoloViewsConverter -from ..util import is_list_like, process_dynamic_args +from ..util import is_list_like, process_dynamic_args, import_geoviews # Color palette for examples: https://www.color-hex.com/color-palette/1018056 # light green: #55a194 @@ -1356,12 +1356,8 @@ def windbarbs(self, x=None, y=None, angle=None, mag=None, **kwds): - GeoViews: https://geoviews.org/gallery/bokeh/wind_barbs_example.html - Matplotlib: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barbs.html """ - try: - import geoviews as gv # noqa: F401 - except ImportError: - raise ImportError( - 'geoviews>=1.15.0 is required for windbarbs plots. Please install geoviews to use this feature.' - ) + gv = import_geoviews() + HoloViewsConverter._kind_mapping['windbarbs'] = gv.WindBarbs return self(x, y, angle=angle, mag=mag, kind='windbarbs', **kwds) def vectorfield(self, x=None, y=None, angle=None, mag=None, **kwds): diff --git a/hvplot/util.py b/hvplot/util.py index eb08acbbd..a1a286c5b 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -1121,6 +1121,12 @@ def __call__(self): for cls in [hvPlot, hvPlotTabular]: for _kind in HoloViewsConverter._kind_mapping: if hasattr(cls, _kind): + # Handle dynamically added kinds (e.g., 'windbarbs' is added + # to _kind_mapping at runtime when first called) + if (cls, _kind) not in self.orig: + method = getattr(cls, _kind) + sig = inspect.signature(method) + self.orig[(cls, _kind)] = (sig, method.__doc__) signature = self.orig[(cls, _kind)][0] _patch_doc(cls, _kind, signature=signature) From 9dae4fa775720fa2e94dc4cd30a5f2917f85936e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 16:13:20 -0800 Subject: [PATCH 09/10] Fix tests --- hvplot/plotting/core.py | 7 +++++++ hvplot/tests/plotting/testcore.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index 9a6a78829..b71ad0fd7 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -101,6 +101,13 @@ def _get_converter(self, x=None, y=None, kind=None, **kwds): x = x or params.pop('x', None) y = y or params.pop('y', None) kind = kind or params.pop('kind', None) + + # Ensure windbarbs is registered in _kind_mapping (it's added dynamically + # when geoviews is available) + if kind == 'windbarbs' and 'windbarbs' not in HoloViewsConverter._kind_mapping: + gv = import_geoviews() + HoloViewsConverter._kind_mapping['windbarbs'] = gv.WindBarbs + return HoloViewsConverter(self._data, x, y, kind=kind, **params) def __dir__(self): diff --git a/hvplot/tests/plotting/testcore.py b/hvplot/tests/plotting/testcore.py index 3e5986d54..3d80a7525 100644 --- a/hvplot/tests/plotting/testcore.py +++ b/hvplot/tests/plotting/testcore.py @@ -28,7 +28,7 @@ class pl: TYPES = {t for t in dir(hvPlotTabular) if not t.startswith('_') and t != 'explorer'} -FRAME_TYPES = TYPES - {'bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield'} +FRAME_TYPES = TYPES - {'bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield', 'windbarbs'} SERIES_TYPES = FRAME_TYPES - {'points', 'polygons', 'ohlc', 'paths'} frame_kinds = pytest.mark.parametrize('kind', sorted(FRAME_TYPES)) series_kinds = pytest.mark.parametrize('kind', sorted(SERIES_TYPES)) From 892ea2960d9c579f70448349ab6339fd8b122eae Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 23 Dec 2025 16:22:45 -0800 Subject: [PATCH 10/10] Add error handling for both --- hvplot/converter.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/hvplot/converter.py b/hvplot/converter.py index e4ddbee4b..ac16b539e 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -3290,6 +3290,12 @@ def windbarbs(self, x=None, y=None, angle=None, mag=None, data=None): angle = self.kwds.get('angle') mag = self.kwds.get('mag') + + if (angle is None) != (mag is None): + raise ValueError("windbarbs requires either both 'angle' and 'mag' or neither") + if angle is None and mag is None: + raise ValueError("windbarbs requires 'angle' and 'mag' parameters") + z = [angle, mag] + self.hover_cols redim = self._merge_redim({z[1]: self._dim_ranges['c']}) params = dict(self._relabel) @@ -3316,6 +3322,12 @@ def vectorfield(self, x=None, y=None, angle=None, mag=None, data=None): angle = self.kwds.get('angle') mag = self.kwds.get('mag') + + if (angle is None) != (mag is None): + raise ValueError("vectorfield requires either both 'angle' and 'mag' or neither") + if angle is None and mag is None: + raise ValueError("vectorfield requires 'angle' and 'mag' parameters") + z = [angle, mag] + self.hover_cols redim = self._merge_redim({z[1]: self._dim_ranges['c']}) params = dict(self._relabel)