diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 636086041..6ca7e1eb0 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -105,7 +105,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - name: install - run: pip install ."[doc, examples, geo]" + run: pip install -v --prefer-binary -e ."[doc, examples, geo]" - name: install dev nbsite run: pip install --pre -U nbsite - name: pip list diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1af50a7b0..8d8706ebd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -60,10 +60,10 @@ jobs: run: | MATRIX=$(jq -nsc '{ "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "python-version": ["3.8", "3.12"], + "python-version": ["3.9", "3.12"], "exclude": [ { - "python-version": "3.8", + "python-version": "3.9", "os": "macos-latest" } ] @@ -74,7 +74,7 @@ jobs: run: | MATRIX=$(jq -nsc '{ "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "python-version": ["3.8", "3.12"], + "python-version": ["3.9", "3.12"], "include": [ { "python-version": "3.9", @@ -91,7 +91,7 @@ jobs: ], "exclude": [ { - "python-version": "3.8", + "python-version": "3.9", "os": "macos-latest" } ] @@ -159,13 +159,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: upgrade pip / setuptools run: pip install -U pip setuptools - - name: install without geo - # Because cartopy cannot be installed on Python 3.8 on these platforms - if: matrix.python-version == '3.8' && contains(fromJSON('["ubuntu-latest", "windows-latest"]'), matrix.os) - run: pip install -ve '.[tests, examples-tests, hvdev, dev-extras]' - name: install with geo - if: matrix.python-version != '3.8' || !contains(fromJSON('["ubuntu-latest", "windows-latest"]'), matrix.os) - run: pip install -ve '.[tests, examples-tests, geo, hvdev, hvdev-geo, dev-extras]' + run: pip install -v --prefer-binary -e '.[tests, examples-tests, geo, hvdev, hvdev-geo, dev-extras]' - name: pip list run: pip list - name: bokeh sampledata diff --git a/doc/developer_guide/index.md b/doc/developer_guide/index.md index 292f7b70b..92808eea8 100644 --- a/doc/developer_guide/index.md +++ b/doc/developer_guide/index.md @@ -79,7 +79,7 @@ source .venv/bin/activate Install the test dependencies: ``` bash -pip install -e '.[tests, examples-tests, geo, hvdev, hvdev-geo, dev-extras]' +pip install --prefer-binary -e '.[tests, examples-tests, geo, hvdev, hvdev-geo, dev-extras]' ``` ::: diff --git a/doc/getting_started/installation.md b/doc/getting_started/installation.md index 44f9861bd..7f590bc22 100644 --- a/doc/getting_started/installation.md +++ b/doc/getting_started/installation.md @@ -5,7 +5,7 @@ | Latest release | [![Github release](https://img.shields.io/github/release/holoviz/hvplot.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/hvplot/releases) [![PyPI version](https://img.shields.io/pypi/v/hvplot.svg?colorB=cc77dd)](https://pypi.python.org/pypi/hvplot) [![hvplot version](https://img.shields.io/conda/v/pyviz/hvplot.svg?colorB=4488ff&style=flat)](https://anaconda.org/pyviz/hvplot) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/hvplot.svg?label=conda%7Cconda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/hvplot) [![defaults version](https://img.shields.io/conda/v/anaconda/hvplot.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/anaconda/hvplot) | | Python | [![Python support](https://img.shields.io/pypi/pyversions/hvplot.svg)](https://pypi.org/project/hvplot/) | -hvPlot supports Python 3.8 and above on Linux, Windows, or Mac. hvPlot can be installed with [conda](https://conda.io/en/latest/): +hvPlot supports Python 3.9 and above on Linux, Windows, or Mac. hvPlot can be installed with [conda](https://conda.io/en/latest/): conda install hvplot diff --git a/doc/reference/pandas/paths.ipynb b/doc/reference/pandas/paths.ipynb new file mode 100644 index 000000000..395f21be4 --- /dev/null +++ b/doc/reference/pandas/paths.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import hvplot.pandas # noqa\n", + "import cartopy.crs as ccrs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Paths are useful if you are plotting lines on a geographic map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame({\"city\": [\"NY\", \"Delhi\"], \"lon\": [-75, 77.23], \"lat\": [43, 28.61]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice how the line in blue between New York and Delhi is not straight on a flat PlateCarree map, this is because the Geodetic coordinate system is a truly spherical coordinate system, where a line between two points is defined as the shortest path between those points on the globe rather than 2d Cartesian space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "common_kwargs = dict(\n", + " x=\"lon\",\n", + " y=\"lat\",\n", + " geo=True,\n", + " project=True,\n", + " projection=ccrs.GOOGLE_MERCATOR,\n", + " global_extent=True\n", + ")\n", + "shortest_path = df.hvplot.paths(color=\"blue\", crs=ccrs.Geodetic(), tiles=True, **common_kwargs)\n", + "straight_path = df.hvplot.paths(color=\"grey\", line_dash=\"dashed\", **common_kwargs)\n", + "labels = df.hvplot.labels(text_color=\"black\", text=\"city\", **common_kwargs)\n", + "shortest_path * straight_path * labels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Example adapted from https://scitools.org.uk/cartopy/docs/latest/matplotlib/intro.html." + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/user_guide/Customization.ipynb b/doc/user_guide/Customization.ipynb index 14806f48a..8d1c92a48 100644 --- a/doc/user_guide/Customization.ipynb +++ b/doc/user_guide/Customization.ipynb @@ -154,6 +154,29 @@ " Aggregator to use when applying rasterize or datashade operation\n", " (valid options include 'mean', 'count', 'min', 'max' and more, and\n", " datashader reduction objects)\n", + " downsample (default=False):\n", + " Controls the application of downsampling to the plotted data,\n", + " which is particularly useful for large timeseries datasets to\n", + " reduce the amount of data sent to browser and improve\n", + " visualization performance. Requires HoloViews >= 1.16. Additional\n", + " dependencies: Installing the `tsdownsample` library is required\n", + " for using any downsampling methods other than the default 'lttb'.\n", + " Acceptable values:\n", + " - False: No downsampling is applied.\n", + " - True: Applies downsampling using HoloViews' default algorithm\n", + " (LTTB - Largest Triangle Three Buckets).\n", + " - 'lttb': Explicitly applies the Largest Triangle Three Buckets\n", + " algorithm.\n", + " - 'minmax': Applies the MinMax algorithm, selecting the minimum\n", + " and maximum values in each bin. Requires `tsdownsample`.\n", + " - 'm4': Applies the M4 algorithm, selecting the minimum, maximum,\n", + " first, and last values in each bin. Requires `tsdownsample`.\n", + " - 'minmax-lttb': Combines MinMax and LTTB algorithms for\n", + " downsampling, first applying MinMax to reduce to a preliminary\n", + " set of points, then LTTB for further reduction. Requires\n", + " `tsdownsample`.\n", + " Other string values corresponding to supported algorithms in\n", + " HoloViews may also be used.\n", " dynamic (default=True):\n", " Whether to return a dynamic plot which sends updates on widget and\n", " zoom/pan events or whether all the data should be embedded\n", @@ -168,6 +191,10 @@ " rasterize (default=False):\n", " Whether to apply rasterization using the datashader library\n", " returning an aggregated Image\n", + " resample_when (default=None):\n", + " Applies a resampling operation (datashade, rasterize or downsample) if\n", + " the number of individual data points present in the current zoom range\n", + " is above this threshold. The raw plot is displayed otherwise.\n", " x_sampling/y_sampling (default=None):\n", " Specifies the smallest allowed sampling interval along the x/y axis." ] diff --git a/doc/user_guide/Large_Timeseries.ipynb b/doc/user_guide/Large_Timeseries.ipynb index ed1d31bb6..e2fe2798d 100644 --- a/doc/user_guide/Large_Timeseries.ipynb +++ b/doc/user_guide/Large_Timeseries.ipynb @@ -256,6 +256,29 @@ " min_height=300, autorange='y', title=\"Datashader Rasterize\", colorbar=False, line_width=2)" ] }, + { + "cell_type": "markdown", + "id": "5a98e727", + "metadata": {}, + "source": [ + "### Rasterize Conditionally\n", + "\n", + "Alternatively, it's possible to activate `rasterize` *conditionally* with `resample_when`.\n", + "\n", + "When the number of individual data points present in the current zoom range is below the provided threshold, the raw plot is displayed; otherwise the `rasterize`, `datashade`, or `downsample` operation is applied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12910d8e", + "metadata": {}, + "outputs": [], + "source": [ + "df0.hvplot(x=\"time\", y=\"value\", rasterize=True, resample_when=1000, cnorm='eq_hist', padding=(0, 0.1),\n", + " min_height=300, autorange='y', title=\"Datashader Rasterize\", colorbar=False, line_width=2)" + ] + }, { "cell_type": "markdown", "id": "naughty-adventure", diff --git a/doc/user_guide/NetworkX.ipynb b/doc/user_guide/NetworkX.ipynb index 24eebf27f..349581102 100644 --- a/doc/user_guide/NetworkX.ipynb +++ b/doc/user_guide/NetworkX.ipynb @@ -239,7 +239,7 @@ "print(\"source vertex {target:length, }\")\n", "for v in G.nodes():\n", " spl = dict(nx.single_source_shortest_path_length(G, v))\n", - " print('{} {} '.format(v, spl))\n", + " print(f'{v} {spl} ')\n", " for p in spl:\n", " pathlengths.append(spl[p])\n", "\n", diff --git a/envs/py3.10-tests.yaml b/envs/py3.10-tests.yaml index d9111cf8e..fb698daf3 100644 --- a/envs/py3.10-tests.yaml +++ b/envs/py3.10-tests.yaml @@ -14,7 +14,7 @@ channels: - conda-forge dependencies: - python=3.10 - - bokeh>=1.0.0 + - bokeh>=3.1 - cartopy - colorcet>=2 - dask @@ -26,7 +26,7 @@ dependencies: - geodatasets>=2023.12.0 - geopandas - geoviews-core>=1.9.0 - - holoviews>=1.11.0 + - holoviews>=1.19.0 - ibis-duckdb - intake-parquet>=0.2.3 - intake-xarray>=0.5.0 @@ -38,10 +38,10 @@ dependencies: - networkx>=2.6.3 - notebook>=5.4 - numba>=0.51.0 - - numpy>=1.15 + - numpy>=1.21 - packaging - - pandas - - panel>=0.11.0 + - pandas>=1.3 + - panel>=1.0 - param<3.0,>=1.12.0 - parameterized - pillow>=8.2.0 diff --git a/envs/py3.11-docs.yaml b/envs/py3.11-docs.yaml index 42f4f61b3..a0b589e6e 100644 --- a/envs/py3.11-docs.yaml +++ b/envs/py3.11-docs.yaml @@ -14,7 +14,7 @@ channels: - conda-forge dependencies: - python=3.11 - - bokeh>=1.0.0 + - bokeh>=3.1 - cartopy - colorcet>=2 - dask>=2021.3.0 @@ -25,7 +25,7 @@ dependencies: - geodatasets>=2023.12.0 - geopandas - geoviews-core>=1.9.0 - - holoviews>=1.11.0 + - holoviews>=1.19.0 - ibis-duckdb - intake-parquet>=0.2.3 - intake-xarray>=0.5.0 @@ -37,10 +37,10 @@ dependencies: - networkx>=2.6.3 - notebook>=5.4 - numba>=0.51.0 - - numpy>=1.15 + - numpy>=1.21 - packaging - - pandas - - panel>=0.11.0 + - pandas>=1.3 + - panel>=1.0 - param<3.0,>=1.12.0 - pillow>=8.2.0 - plotly diff --git a/envs/py3.11-tests.yaml b/envs/py3.11-tests.yaml index ffd119a79..b6ef11098 100644 --- a/envs/py3.11-tests.yaml +++ b/envs/py3.11-tests.yaml @@ -14,7 +14,7 @@ channels: - conda-forge dependencies: - python=3.11 - - bokeh>=1.0.0 + - bokeh>=3.1 - cartopy - colorcet>=2 - dask @@ -26,7 +26,7 @@ dependencies: - geodatasets>=2023.12.0 - geopandas - geoviews-core>=1.9.0 - - holoviews>=1.11.0 + - holoviews>=1.19.0 - ibis-duckdb - intake-parquet>=0.2.3 - intake-xarray>=0.5.0 @@ -38,10 +38,10 @@ dependencies: - networkx>=2.6.3 - notebook>=5.4 - numba>=0.51.0 - - numpy>=1.15 + - numpy>=1.21 - packaging - - pandas - - panel>=0.11.0 + - pandas>=1.3 + - panel>=1.0 - param<3.0,>=1.12.0 - parameterized - pillow>=8.2.0 diff --git a/envs/py3.12-tests.yaml b/envs/py3.12-tests.yaml index 88fe00ad0..43c791d56 100644 --- a/envs/py3.12-tests.yaml +++ b/envs/py3.12-tests.yaml @@ -14,7 +14,7 @@ channels: - conda-forge dependencies: - python=3.12 - - bokeh>=1.0.0 + - bokeh>=3.1 - cartopy - colorcet>=2 - dask @@ -26,7 +26,7 @@ dependencies: - geodatasets>=2023.12.0 - geopandas - geoviews-core>=1.9.0 - - holoviews>=1.11.0 + - holoviews>=1.19.0 - ibis-duckdb - intake-parquet>=0.2.3 - intake-xarray>=0.5.0 @@ -38,10 +38,10 @@ dependencies: - networkx>=2.6.3 - notebook>=5.4 - numba>=0.51.0 - - numpy>=1.15 + - numpy>=1.21 - packaging - - pandas - - panel>=0.11.0 + - pandas>=1.3 + - panel>=1.0 - param<3.0,>=1.12.0 - parameterized - pillow>=8.2.0 diff --git a/envs/py3.8-tests.yaml b/envs/py3.8-tests.yaml deleted file mode 100644 index 077eb3b5f..000000000 --- a/envs/py3.8-tests.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# -# This file is autogenerated by pyproject2conda -# with the following command: -# -# $ pyproject2conda project --overwrite force --template-python envs/py{py_version}-{env} -# -# You should not manually edit this file. -# Instead edit the corresponding pyproject.toml file. -# -name: hvplottests -channels: - - nodefaults - - pyviz/label/dev - - conda-forge -dependencies: - - python=3.8 - - bokeh>=1.0.0 - - cartopy - - colorcet>=2 - - dask - - dask>=2021.3.0 - - datashader>=0.6.5 - - fiona - - fugue - - fugue-sql-antlr>=0.2.0 - - geodatasets>=2023.12.0 - - geopandas - - geoviews-core>=1.9.0 - - holoviews>=1.11.0 - - ibis-duckdb - - intake-parquet>=0.2.3 - - intake-xarray>=0.5.0 - - intake<2.0.0,>=0.6.5 - - ipywidgets - - jinja2 - - matplotlib - - nbval - - networkx>=2.6.3 - - notebook>=5.4 - - numba>=0.51.0 - - numpy>=1.15 - - packaging - - pandas - - panel>=0.11.0 - - param<3.0,>=1.12.0 - - parameterized - - pillow>=8.2.0 - - plotly - - polars - - pooch - - pooch>=1.6.0 - - pre-commit - - pygraphviz - - pyproj - - pytest - - pytest-cov - - pytest-xdist - - qpd>=0.4.4 - - rasterio - - rioxarray - - ruff - - s3fs>=2022.1.0 - - scikit-image>=0.17.2 - - scipy - - scipy>=1.5.3 - - selenium>=3.141.0 - - setuptools_scm>=6 - - spatialpandas>=0.4.3 - - sqlglot - - streamz>=0.3.0 - - xarray - - xarray>=0.18.2 - - xyzservices>=2022.9.0 - - pip - - pip: - - -e .. diff --git a/envs/py3.9-tests.yaml b/envs/py3.9-tests.yaml index 28e233691..bd89d3556 100644 --- a/envs/py3.9-tests.yaml +++ b/envs/py3.9-tests.yaml @@ -14,7 +14,7 @@ channels: - conda-forge dependencies: - python=3.9 - - bokeh>=1.0.0 + - bokeh>=3.1 - cartopy - colorcet>=2 - dask @@ -26,7 +26,7 @@ dependencies: - geodatasets>=2023.12.0 - geopandas - geoviews-core>=1.9.0 - - holoviews>=1.11.0 + - holoviews>=1.19.0 - ibis-duckdb - intake-parquet>=0.2.3 - intake-xarray>=0.5.0 @@ -38,10 +38,10 @@ dependencies: - networkx>=2.6.3 - notebook>=5.4 - numba>=0.51.0 - - numpy>=1.15 + - numpy>=1.21 - packaging - - pandas - - panel>=0.11.0 + - pandas>=1.3 + - panel>=1.0 - param<3.0,>=1.12.0 - parameterized - pillow>=8.2.0 diff --git a/hvplot/converter.py b/hvplot/converter.py index 3a44934e4..cda7f558f 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -47,13 +47,11 @@ from holoviews.operation import histogram, apply_when from holoviews.streams import Buffer, Pipe from holoviews.util.transform import dim -from packaging.version import Version from pandas import DatetimeIndex, MultiIndex from .backend_transforms import _transfer_opts_cur_backend from .util import ( filter_opts, - hv_version, is_tabular, is_series, is_dask, @@ -701,12 +699,8 @@ def __init__( plot_opts['xlim'] = tuple(xlim) if ylim is not None: plot_opts['ylim'] = tuple(ylim) - if autorange is not None: - if hv_version < Version('1.16.0'): - param.main.param.warning('autorange option requires HoloViews >= 1.16') - else: - plot_opts['autorange'] = autorange + plot_opts['autorange'] = autorange self.invert = invert if loglog is not None: @@ -728,7 +722,7 @@ def __init__( elif legend not in (True, False, None): raise ValueError( 'The legend option should be a boolean or ' - 'a valid legend position (i.e. one of %s).' % list(self._legend_positions) + f'a valid legend position (i.e. one of {list(self._legend_positions)}).' ) plotwds = [ 'xticks', @@ -844,7 +838,7 @@ def __init__( pass if cnorm is not None: plot_opts['cnorm'] = cnorm - if rescale_discrete_levels is not None and hv_version >= Version('1.15.0'): + if rescale_discrete_levels is not None: plot_opts['rescale_discrete_levels'] = rescale_discrete_levels self._plot_opts = plot_opts @@ -1154,7 +1148,7 @@ def _process_data( self.data = data else: - raise ValueError('Supplied data type %s not understood' % type(data).__name__) + raise ValueError(f'Supplied data type {type(data).__name__} not understood') if stream is not None: if streaming: @@ -1166,7 +1160,7 @@ def _process_data( elif isinstance(stream, Buffer): self.stream_type = 'streaming' else: - raise ValueError('Stream of type %s not recognized.' % type(stream)) + raise ValueError(f'Stream of type {type(stream)} not recognized.') streaming = True # Validate data and arguments @@ -1738,10 +1732,7 @@ def method_wrapper(ds, x, y): opts['rescale_discrete_levels'] = self._plot_opts['rescale_discrete_levels'] elif self.rasterize: operation = rasterize - if Version(hv.__version__) < Version('1.18.0a1'): - eltype = 'Image' - else: - eltype = 'ImageStack' if categorical else 'Image' + eltype = 'ImageStack' if categorical else 'Image' if 'cmap' in self._style_opts: style['cmap'] = self._style_opts['cmap'] if self._dim_ranges.get('c', (None, None)) != (None, None): @@ -1786,8 +1777,8 @@ def _apply_layers(self, obj): coastline = coastline.opts(scale=self.coastline) elif self.coastline is not True: param.main.param.warning( - 'coastline scale of %s not recognized, must be one ' - "'10m', '50m' or '110m'." % self.coastline + 'coastline scale of {self.coastline} not recognized, must be one ' + "'10m', '50m' or '110m'." ) obj = obj * coastline.opts(projection=self.output_projection) @@ -1798,17 +1789,17 @@ def _apply_layers(self, obj): feature_obj = getattr(gv.feature, feature) if feature_obj is None: raise ValueError( - 'Feature %r was not recognized, must be one of ' + f'Feature {feature!r} was not recognized, must be one of ' "'borders', 'coastline', 'lakes', 'land', 'ocean', " - "'rivers' and 'states'." % feature + "'rivers' and 'states'." ) feature_obj = feature_obj.clone() if isinstance(self.features, dict): scale = self.features[feature] if scale not in ['10m', '50m', '110m']: param.main.param.warning( - 'Feature scale of %r not recognized, ' - "must be one of '10m', '50m' or '110m'." % scale + f'Feature scale of {scale} not recognized, ' + "must be one of '10m', '50m' or '110m'." ) else: feature_obj = feature_obj.opts(scale=scale) @@ -2572,6 +2563,7 @@ def labels(self, x=None, y=None, data=None): if not text: text = [c for c in data.columns if c not in (x, y)][0] elif text not in data.columns: + data = data.copy() template_str = text # needed for dask lazy compute data['label'] = data.apply(lambda row: template_str.format(**row), axis=1) text = 'label' @@ -2579,10 +2571,13 @@ def labels(self, x=None, y=None, data=None): kdims, vdims = self._get_dimensions([x, y], [text]) cur_opts, compat_opts = self._get_compat_opts('Labels') element = self._get_element('labels') - return ( - element(data, kdims, vdims) - .redim(**self._redim) - .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts) + if self.by: + labels = Dataset(data).to(element, kdims, vdims, self.by) + labels = labels.layout() if self.subplots else labels.overlay(sort=False) + else: + labels = element(data, kdims, vdims) + return labels.redim(**self._redim).apply( + self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts ) ########################## diff --git a/hvplot/fugue.py b/hvplot/fugue.py index f280a922b..b06932264 100644 --- a/hvplot/fugue.py +++ b/hvplot/fugue.py @@ -2,7 +2,7 @@ Experimental support for fugue. """ -from typing import Any, Dict, Tuple +from typing import Any import panel as _pn @@ -40,7 +40,7 @@ def process(self, dfs: DataFrames) -> None: charts = [] for df in dfs.values(): params = dict(self.params) - opts: Dict[str, Any] = params.pop('opts', {}) + opts: dict[str, Any] = params.pop('opts', {}) chart = getattr(df.as_pandas().hvplot, self._func)(**params).opts(**opts) charts.append(chart) col = _pn.Column(*charts) @@ -55,7 +55,7 @@ def process(self, dfs: DataFrames) -> None: display(col) # in notebook @parse_outputter.candidate(namespace_candidate(name, lambda x: isinstance(x, str))) - def _parse_hvplot(obj: Tuple[str, str]) -> Outputter: + def _parse_hvplot(obj: tuple[str, str]) -> Outputter: return _Visualize(obj[1]) post_patch(extension, logo) diff --git a/hvplot/interactive.py b/hvplot/interactive.py index ef9490cf2..3d403ef5c 100644 --- a/hvplot/interactive.py +++ b/hvplot/interactive.py @@ -98,7 +98,6 @@ import sys from functools import partial -from packaging.version import Version from types import FunctionType, MethodType import holoviews as hv @@ -106,14 +105,13 @@ import panel as pn import param -from panel.layout import Column, Row, VSpacer, HSpacer +from panel.layout import Column, Row, HSpacer from panel.util import get_method_owner, full_groupby from panel.widgets.base import Widget from .converter import HoloViewsConverter from .util import ( _flatten, - bokeh3, is_tabular, is_xarray, is_xarray_dataarray, @@ -149,11 +147,6 @@ def _find_widgets(op): ): widgets.append(op_arg.owner) if isinstance(op_arg, slice): - if Version(hv.__version__) < Version('1.15.1'): - raise ValueError( - 'Using interactive with slices needs to have ' - 'Holoviews 1.15.1 or greater installed.' - ) nested_op = {'args': [op_arg.start, op_arg.stop, op_arg.step], 'kwargs': {}} for widget in _find_widgets(nested_op): if widget not in widgets: @@ -792,62 +785,6 @@ def layout(self, **kwargs): to the center and widget location specified in the interactive call. """ - if bokeh3: - return self._layout_bk3(**kwargs) - return self._layout_bk2(**kwargs) - - def _layout_bk2(self, **kwargs): - widget_box = self.widgets() - panel = self.output() - loc = self._loc - if loc in ('left', 'right'): - widgets = Column(VSpacer(), widget_box, VSpacer()) - elif loc in ('top', 'bottom'): - widgets = Row(HSpacer(), widget_box, HSpacer()) - elif loc in ('top_left', 'bottom_left'): - widgets = Row(widget_box, HSpacer()) - elif loc in ('top_right', 'bottom_right'): - widgets = Row(HSpacer(), widget_box) - elif loc in ('left_top', 'right_top'): - widgets = Column(widget_box, VSpacer()) - elif loc in ('left_bottom', 'right_bottom'): - widgets = Column(VSpacer(), widget_box) - # TODO: add else and raise error - center = self._center - if not widgets: - if center: - components = [HSpacer(), panel, HSpacer()] - else: - components = [panel] - elif center: - if loc.startswith('left'): - components = [widgets, HSpacer(), panel, HSpacer()] - elif loc.startswith('right'): - components = [HSpacer(), panel, HSpacer(), widgets] - elif loc.startswith('top'): - components = [ - HSpacer(), - Column(widgets, Row(HSpacer(), panel, HSpacer())), - HSpacer(), - ] - elif loc.startswith('bottom'): - components = [ - HSpacer(), - Column(Row(HSpacer(), panel, HSpacer()), widgets), - HSpacer(), - ] - else: - if loc.startswith('left'): - components = [widgets, panel] - elif loc.startswith('right'): - components = [panel, widgets] - elif loc.startswith('top'): - components = [Column(widgets, panel)] - elif loc.startswith('bottom'): - components = [Column(panel, widgets)] - return Row(*components, **kwargs) - - def _layout_bk3(self, **kwargs): widget_box = self.widgets() panel = self.output() loc = self._loc diff --git a/hvplot/networkx.py b/hvplot/networkx.py index 1f7ffd6c9..06e648bdd 100644 --- a/hvplot/networkx.py +++ b/hvplot/networkx.py @@ -7,7 +7,6 @@ from bokeh.models import HoverTool from holoviews import Graph, Labels, dim from holoviews.core.options import Store -from holoviews.core.util import dimension_sanitizer from holoviews.plotting.bokeh import GraphPlot, LabelsPlot from holoviews.plotting.bokeh.styles import markers @@ -332,7 +331,7 @@ def draw(G, pos=None, **kwargs): (d.label, d.name + '_values' if d in g.kdims else d.name) for d in g.kdims + g.vdims ] tooltips = [ - (label, '@{%s}' % dimension_sanitizer(name)) + (label, '@{{dimension_sanitizer(name)}}') for label, name in tooltip_dims if name not in node_styles + edge_styles ] diff --git a/hvplot/plotting/scatter_matrix.py b/hvplot/plotting/scatter_matrix.py index 0b1d6d0e3..e9ef0b162 100644 --- a/hvplot/plotting/scatter_matrix.py +++ b/hvplot/plotting/scatter_matrix.py @@ -1,5 +1,4 @@ from functools import partial -import warnings import holoviews as _hv import numpy as _np @@ -106,14 +105,6 @@ def scatter_matrix( import datashader # noqa except ImportError: raise ImportError('rasterize and datashade require datashader to be installed.') - from ..util import hv_version - - if hv_version <= Version('1.14.6'): - warnings.warn( - 'Versions of holoviews before 1.14.7 did not support ' - 'dynamic update of rasterized/datashaded scatter matrix. ' - 'Update holoviews to a newer version.' - ) if rasterize and datashade: raise ValueError('Choose to either rasterize or datashade the scatter matrix, not both.') diff --git a/hvplot/tests/conftest.py b/hvplot/tests/conftest.py index 1c0b766de..9db13f17b 100644 --- a/hvplot/tests/conftest.py +++ b/hvplot/tests/conftest.py @@ -11,9 +11,7 @@ def pytest_addoption(parser): for marker, info in optional_markers.items(): - parser.addoption( - '--{}'.format(marker), action='store_true', default=False, help=info['help'] - ) + parser.addoption(f'--{marker}', action='store_true', default=False, help=info['help']) def pytest_configure(config): diff --git a/hvplot/tests/testcharts.py b/hvplot/tests/testcharts.py index bc4c3673c..4c61d1205 100644 --- a/hvplot/tests/testcharts.py +++ b/hvplot/tests/testcharts.py @@ -4,7 +4,7 @@ from parameterized import parameterized from holoviews.core.dimension import Dimension -from holoviews import NdOverlay, Store, dim, render +from holoviews import NdLayout, NdOverlay, Store, dim, render from holoviews.element import Curve, Area, Scatter, Points, Path, HeatMap from holoviews.element.comparison import ComparisonTestCase @@ -413,6 +413,7 @@ def test_labels_format(self): plot = self.df.hvplot('x', 'y', text='({x}, {y})', kind='labels') assert list(plot.dimensions()) == [Dimension('x'), Dimension('y'), Dimension('label')] assert list(plot.data['label']) == ['(1, 2)', '(3, 4)', '(5, 6)'] + assert 'label' not in self.df def test_labels_no_format_edge_case(self): plot = self.edge_df.hvplot.labels('Longitude', 'Latitude') @@ -434,6 +435,22 @@ def test_labels_format_float(self): ] assert list(plot.data['label']) == ['-58.7E -34.58N', '-47.9E -15.78N', '-70.7E -33.45N'] + def test_labels_by(self): + plot = self.edge_df.hvplot.labels( + 'Longitude', 'Latitude', text='{Longitude:.1f}E {Latitude:.2f}N', by='Volume {m3}' + ) + assert isinstance(plot, NdOverlay) + + def test_labels_by_subplots(self): + plot = self.edge_df.hvplot.labels( + 'Longitude', + 'Latitude', + text='{Longitude:.1f}E {Latitude:.2f}N', + by='Volume {m3}', + subplots=True, + ) + assert isinstance(plot, NdLayout) + class TestChart1DDask(TestChart1D): def setUp(self): diff --git a/hvplot/tests/testinteractive.py b/hvplot/tests/testinteractive.py index 6ddf62d72..cf61d9cf7 100644 --- a/hvplot/tests/testinteractive.py +++ b/hvplot/tests/testinteractive.py @@ -1,5 +1,3 @@ -from packaging.version import Version - import holoviews as hv import hvplot.pandas # noqa import hvplot.xarray # noqa @@ -15,10 +13,6 @@ from hvplot.interactive import Interactive from hvplot.tests.util import makeDataFrame, makeMixedDataFrame from hvplot.xarray import XArrayInteractive -from hvplot.util import bokeh3, param2 - -is_bokeh2 = pytest.mark.skipif(bokeh3, reason='requires bokeh 2.x') -is_bokeh3 = pytest.mark.skipif(not bokeh3, reason='requires bokeh 3.x') @pytest.fixture(scope='module') @@ -208,10 +202,6 @@ def test_interactive_nested_widgets(): assert iw[0] == w -@pytest.mark.skipif( - Version(hv.__version__) < Version('1.15.1'), - reason='Needs holoviews 1.15.1', -) def test_interactive_slice(): df = makeDataFrame() w = pn.widgets.IntSlider(start=10, end=40) @@ -1264,31 +1254,6 @@ def test_interactive_pandas_layout_default_no_widgets_kwargs(df): assert layout.width == 200 -@is_bokeh2 -def test_interactive_pandas_layout_default_with_widgets(df): - w = pn.widgets.IntSlider(value=2, start=1, end=5) - dfi = Interactive(df) - dfi = dfi.head(w) - - assert dfi._center is False - assert dfi._loc == 'top_left' - - layout = dfi.layout() - - assert isinstance(layout, pn.Row) - assert len(layout) == 1 - assert isinstance(layout[0], pn.Column) - assert len(layout[0]) == 2 - assert isinstance(layout[0][0], pn.Row) - assert isinstance(layout[0][1], pn.pane.PaneBase) - assert len(layout[0][0]) == 2 - assert isinstance(layout[0][0][0], pn.Column) - assert len(layout[0][0][0]) == 1 - assert isinstance(layout[0][0][0][0], pn.widgets.Widget) - assert isinstance(layout[0][0][1], pn.layout.HSpacer) - - -@is_bokeh3 def test_interactive_pandas_layout_default_with_widgets_bk3(df): w = pn.widgets.IntSlider(value=2, start=1, end=5) dfi = Interactive(df) @@ -1309,59 +1274,6 @@ def test_interactive_pandas_layout_default_with_widgets_bk3(df): assert isinstance(layout[0][0][0], pn.widgets.IntSlider) -@is_bokeh2 -def test_interactive_pandas_layout_center_with_widgets(df): - w = pn.widgets.IntSlider(value=2, start=1, end=5) - dfi = df.interactive(center=True) - dfi = dfi.head(w) - - assert dfi._center is True - assert dfi._loc == 'top_left' - - layout = dfi.layout() - - assert isinstance(layout, pn.Row) - assert len(layout) == 3 - assert isinstance(layout[0], pn.layout.HSpacer) - assert isinstance(layout[1], pn.Column) - assert isinstance(layout[2], pn.layout.HSpacer) - assert len(layout[1]) == 2 - assert isinstance(layout[1][0], pn.Row) - assert isinstance(layout[1][1], pn.Row) - assert len(layout[1][0]) == 2 - assert len(layout[1][1]) == 3 - assert isinstance(layout[1][0][0], pn.Column) - assert len(layout[1][0][0]) == 1 - assert isinstance(layout[1][0][0][0], pn.widgets.Widget) - assert isinstance(layout[1][1][0], pn.layout.HSpacer) - assert isinstance(layout[1][1][1], pn.pane.PaneBase) - assert isinstance(layout[1][1][2], pn.layout.HSpacer) - - -@is_bokeh2 -def test_interactive_pandas_layout_loc_with_widgets(df): - w = pn.widgets.IntSlider(value=2, start=1, end=5) - dfi = df.interactive(loc='top_right') - dfi = dfi.head(w) - - assert dfi._center is False - assert dfi._loc == 'top_right' - - layout = dfi.layout() - - assert isinstance(layout, pn.Row) - assert len(layout) == 1 - assert isinstance(layout[0], pn.Column) - assert len(layout[0]) == 2 - assert isinstance(layout[0][0], pn.Row) - assert isinstance(layout[0][1], pn.pane.PaneBase) - assert len(layout[0][0]) == 2 - assert isinstance(layout[0][0][0], pn.layout.HSpacer) - assert isinstance(layout[0][0][1], pn.Column) - assert len(layout[0][0][1]) == 1 - assert isinstance(layout[0][0][1][0], pn.widgets.Widget) - - def test_interactive_pandas_eval(df): dfi = Interactive(df) dfi = dfi.head(2) @@ -1396,10 +1308,7 @@ def test_interactive_pandas_series_widget_value(series): assert isinstance(si._current, pd.DataFrame) pd.testing.assert_series_equal(si._current.A, series + w.value) assert si._obj is series - if param2: - assert "dim('*').pd+= Version('3.0') -param2 = Version(param.__version__) >= Version('2.0rc4') _fugue_ipython = None # To be set to True in tests to mock ipython @@ -613,10 +611,7 @@ def process_dynamic_args(x, y, kind, **kwds): if isinstance(v, param.Parameter): dynamic[k] = v elif panel_available and isinstance(v, pn.widgets.Widget): - if Version(pn.__version__) < Version('0.6.4'): - dynamic[k] = v.param.value - else: - dynamic[k] = v + dynamic[k] = v for k, v in kwds.items(): if k not in dynamic and isinstance(v, FunctionType) and hasattr(v, '_dinfo'): diff --git a/hvplot/utilities.py b/hvplot/utilities.py index e6a6622c3..29657b1e9 100644 --- a/hvplot/utilities.py +++ b/hvplot/utilities.py @@ -96,7 +96,7 @@ def show(obj, title=None, port=0, **kwargs): elif isinstance(obj, _pn.viewable.Viewable): return obj.show(title, port, **kwargs) else: - raise ValueError('%s type object not recognized and cannot be shown.' % type(obj).__name__) + raise ValueError('{type(obj).__name__} type object not recognized and cannot be shown.') class hvplot_extension(_hv.extension): diff --git a/pyproject.toml b/pyproject.toml index c02f5906f..fc27923af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dynamic = ["version"] description = "A high-level plotting API for the PyData ecosystem built on HoloViews." readme = "README.md" license = { text = "BSD" } -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Philipp Rudiger", email = "developers@holoviz.org" }, ] @@ -21,7 +21,6 @@ maintainers = [ classifiers = [ "License :: OSI Approved :: BSD License", "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -34,13 +33,13 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "bokeh >=1.0.0", + "bokeh >=3.1", "colorcet >=2", - "holoviews >=1.11.0", - "numpy >=1.15", + "holoviews >=1.19.0", + "numpy >=1.21", "packaging", - "pandas", - "panel >=0.11.0", + "pandas >=1.3", + "panel >=1.0", "param >=1.12.0,<3.0", ] @@ -197,6 +196,11 @@ write-changes = true [tool.ruff] line-length = 99 +[tool.ruff.lint] +extend-select = [ + "UP", +] + [tool.ruff.format] quote-style = "single" @@ -213,7 +217,7 @@ ibis-framework = { skip = true, packages = "ibis-duckdb" } [tool.pyproject2conda.envs."tests"] channels = ["nodefaults", "pyviz/label/dev", "conda-forge"] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.9", "3.10", "3.11", "3.12"] extras = ["tests", "examples-tests", "geo", "graphviz", "dev-extras"] name = "hvplottests" # reqs = ["-e .."] # Doesn't work