Skip to content

Commit

Permalink
Merge branch 'main' into geo_lim_improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro committed Oct 2, 2023
2 parents a273b5e + 8062d52 commit 29174a7
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- uses: conda-incubator/setup-miniconda@v2
with:
miniconda-version: "latest"
python-version: 3.8
python-version: ${{ matrix.python-version }}
- name: Fetch unshallow
run: git fetch --prune --tags --unshallow -f
- name: Set output
Expand Down
16 changes: 3 additions & 13 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,10 @@ concurrency:

jobs:
pre_commit:
name: Run pre-commit hooks
name: Run pre-commit
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: "1"
- name: set PY
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: pre-commit
uses: pre-commit/[email protected]
- uses: holoviz-dev/holoviz_tasks/[email protected]
test_suite:
name: Pytest on ${{ matrix.os }} with Python ${{ matrix.python-version }}
needs: [pre_commit]
Expand All @@ -45,7 +35,7 @@ jobs:
run:
shell: bash -el {0}
steps:
- uses: holoviz-dev/holoviz_tasks/install@v0.1a15
- uses: holoviz-dev/holoviz_tasks/install@v0.1a17
with:
name: unit_test_suite
python-version: ${{ matrix.python-version }}
Expand Down
8 changes: 0 additions & 8 deletions conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ requirements:
- {{ dep }}
{% endfor %}

test:
imports:
- hvplot
requires:
{% for dep in sdata['extras_require']['tests'] %}
- {{ dep }}
{% endfor %}

about:
home: {{ sdata['url'] }}
summary: {{ sdata['description'] }}
Expand Down
2 changes: 1 addition & 1 deletion examples/user_guide/Explorer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"source": [
"hvPlot API provides a simple and intuitive way to create plots. However when you are exploring data you don't always know in advance the best way to display it, or even what kind of plot would be best to visualize the data. You will very likely embark in an iterative process that implies choosing a kind of plot, setting various options, running some code, and repeat until you're satisfied with the output and the insights you get. The *Explorer* is a *Graphical User Interface* that allows you to easily generate customized plots, which in practice gives you the possibility to **explore** both your data and hvPlot's extensive API.\n",
"\n",
"To create an *Explorer* you pass your data to the high-level `hvplot.explorer` function which returns a [Panel](https://panel.holoviz.org/) layout that can be displayed in a notebook or served in a web application. This object displays on the right-hand side a preview of the plot you are building, and on the left-hand side the various options that you can set to customize the plot.\n",
"To create an *Explorer* you pass your data to the high-level `hvplot.explorer` function which returns a [Panel](https://panel.holoviz.org/) layout that can be displayed in a notebook or served in a web application. This object displays on the right-hand side a preview of the plot you are building, and on the left-hand side the various options that you can set to customize the plot. These options can passed to the constructor if you already have pre-defined some, for example `hvplot.explorer(data, title='Penguins', width=200)`.\n",
"\n",
"Note that for the explorer to be displayed in a notebook you need to load the hvPlot extension, which happens automatically when you execute `import hvplot.pandas`. If instead of building Bokeh plots you would rather build Matplotlib or Plotly plot, simply execute once `hvplot.extension('matplotlib')` or `hvplot.extension('matplotlib')` before displaying the explorer."
]
Expand Down
38 changes: 38 additions & 0 deletions examples/user_guide/Timeseries_Data.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"source": [
"import hvplot.pandas # noqa\n",
"from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature as sst\n",
"\n",
"sst.hvplot()"
]
},
Expand Down Expand Up @@ -56,6 +57,26 @@
"sst.hvplot(xformatter=formatter)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Auto-range\n",
"\n",
"*(Available with HoloViews >= 1.16)*\n",
"\n",
"Automatic ranging, aka auto-ranging, on the data in x or y is supported, making it easy to scale the given axes and fit the entire visible curve after a zoom or pan. Try zooming in on the plot and panning around, the y range nicely adapt to fit the curve. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sst.hvplot(autorange=\"y\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -166,6 +187,23 @@
"source": [
"Note that xarray supports grouping and aggregation using a similar syntax. To learn more about timeseries in xarray, see the [xarray timeseries docs](https://xarray.pydata.org/en/stable/time-series.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Downsample time series\n",
"An option when working with large time series is to downsample the data before plotting it. This can be done with `downsample=True`, which applies the `lttb` (Largest Triangle Three Buckets) algorithm to the data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sst.hvplot(label=\"original\") * sst.hvplot(downsample=True, label=\"downsampled\")"
]
}
],
"metadata": {
Expand Down
69 changes: 51 additions & 18 deletions hvplot/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class HoloViewsConverter:
"""
Generic options
---------------
autorange (default=None): Literal['x', 'y'] | None
Whether to enable auto-ranging along the x- or y-axis when
zooming. Requires HoloViews >= 1.16.
clim: tuple
Lower and upper bound of the color scale
cnorm (default='linear'): str
Expand Down Expand Up @@ -182,7 +185,7 @@ class HoloViewsConverter:
check_symmetric_max (default=1000000):
Size above which to stop checking for symmetry by default on the data.
Datashader options
Downsampling options
------------------
aggregator (default=None):
Aggregator to use when applying rasterize or datashade operation
Expand All @@ -197,6 +200,10 @@ class HoloViewsConverter:
Whether to apply rasterization and shading (colormapping) using
the Datashader library, returning an RGB object instead of
individual points
downsample (default=False):
Whether to apply LTTB (Largest Triangle Three Buckets)
downsampling to the element (note this is only well behaved for
timeseries data).
dynspread (default=False):
For plots generated with datashade=True or rasterize=True,
automatically increase the point size when the data is sparse
Expand Down Expand Up @@ -375,15 +382,16 @@ def __init__(
title=None, xlim=None, ylim=None, clim=None, symmetric=None,
logx=None, logy=None, loglog=None, hover=None, subplots=False,
label=None, invert=False, stacked=False, colorbar=None,
datashade=False, rasterize=False, row=None, col=None,
debug=False, framewise=True, aggregator=None,
projection=None, global_extent=None, geo=False,
precompute=False, flip_xaxis=None, flip_yaxis=None,
datashade=False, rasterize=False, downsample=None,
row=None, col=None, debug=False, framewise=True,
aggregator=None, projection=None, global_extent=None,
geo=False, precompute=False, flip_xaxis=None, flip_yaxis=None,
dynspread=False, hover_cols=[], x_sampling=None,
y_sampling=None, project=False, tools=[], attr_labels=None,
coastline=False, tiles=False, sort_date=True,
check_symmetric_max=1000000, transforms={}, stream=None,
cnorm=None, features=None, rescale_discrete_levels=None, **kwds
cnorm=None, features=None, rescale_discrete_levels=None,
autorange=None, **kwds
):
# Process data and related options
self._redim = fields
Expand Down Expand Up @@ -459,6 +467,7 @@ def __init__(
# Operations
self.datashade = datashade
self.rasterize = rasterize
self.downsample = downsample
self.dynspread = dynspread
self.aggregator = aggregator
self.precompute = precompute
Expand All @@ -484,6 +493,11 @@ def __init__(
if ylim is not None:
plot_opts['ylim'] = tuple(ylim)

if autorange is not None and hv_version < Version('1.16.0'):
param.main.param.warning('autorange option requires HoloViews >= 1.16')
else:
plot_opts['autorange'] = autorange

self.invert = invert
if loglog is not None:
logx = logx or loglog
Expand Down Expand Up @@ -581,7 +595,7 @@ def __init__(
symmetric = self._process_symmetric(symmetric, clim, check_symmetric_max)
if self._style_opts.get('cmap') is None:
# Default to categorical camp if we detect categorical shading
if (self.datashade and (self.aggregator is None or 'count_cat' in str(self.aggregator)) and
if ((self.datashade or self.rasterize) and (self.aggregator is None or 'count_cat' in str(self.aggregator)) and
((self.by and not self.subplots) or
(isinstance(self.y, list) or (self.y is None and len(set(self.variables) - set(self.indexes)) > 1)))):
self._style_opts['cmap'] = self._default_cmaps['categorical']
Expand Down Expand Up @@ -1255,10 +1269,28 @@ def method_wrapper(ds, x, y):
projection = self._plot_opts.get('projection', ccrs.GOOGLE_MERCATOR)
obj = project(obj, projection=projection)

if not (self.datashade or self.rasterize):
if not (self.datashade or self.rasterize or self.downsample):
layers = self._apply_layers(obj)
layers = _transfer_opts_cur_backend(layers)
return layers

opts = dict(dynamic=self.dynamic)
if self._plot_opts.get('width') is not None:
opts['width'] = self._plot_opts['width']
if self._plot_opts.get('height') is not None:
opts['height'] = self._plot_opts['height']

if self.downsample:
from holoviews.operation.downsample import downsample1d

if self.x_sampling:
opts['x_sampling'] = self.x_sampling
if self._plot_opts.get('xlim') is not None:
opts['x_range'] = self._plot_opts['xlim']
layers = downsample1d(obj, **opts)
layers = _transfer_opts_cur_backend(layers)
return layers

try:
from holoviews.operation.datashader import datashade, rasterize, dynspread
from datashader import reductions
Expand All @@ -1268,12 +1300,6 @@ def method_wrapper(ds, x, y):
'It can be installed with:\n conda '
'install datashader')

opts = dict(dynamic=self.dynamic)
if self._plot_opts.get('width') is not None:
opts['width'] = self._plot_opts['width']
if self._plot_opts.get('height') is not None:
opts['height'] = self._plot_opts['height']

categorical = False
if self.by and not self.subplots:
opts['aggregator'] = reductions.count_cat(self.by[0])
Expand Down Expand Up @@ -1303,8 +1329,6 @@ def method_wrapper(ds, x, y):
opts['x_range'] = self._plot_opts['xlim']
if self._plot_opts.get('ylim') is not None:
opts['y_range'] = self._plot_opts['ylim']
if not self.dynamic:
opts['dynamic'] = self.dynamic

if 'cmap' in self._style_opts and self.datashade:
levels = self._plot_opts.get('color_levels')
Expand All @@ -1329,7 +1353,10 @@ def method_wrapper(ds, x, y):
opts['rescale_discrete_levels'] = self._plot_opts['rescale_discrete_levels']
else:
operation = rasterize
eltype = 'Image'
if Version(hv.__version__) < Version('1.18.0a1'):
eltype = 'Image'
else:
eltype = 'ImageStack' if self.by else 'Image'
if 'cmap' in self._style_opts:
style['cmap'] = self._style_opts['cmap']
if self._dim_ranges.get('c', (None, None)) != (None, None):
Expand Down Expand Up @@ -1382,7 +1409,13 @@ def _apply_layers(self, obj):
scale)
else:
feature_obj = feature_obj.opts(scale=scale)
obj = feature_obj.opts(projection=self.proj_crs) * obj

if feature_obj.group in ["Land", "Ocean"]:
# Underlay land/ocean
obj = feature_obj.opts(projection=self.proj_crs) * obj
else:
# overlay everything else
obj = obj * feature_obj.opts(projection=self.proj_crs)

if self.tiles and not self.geo:
tiles = self._get_tiles(
Expand Down
5 changes: 5 additions & 0 deletions hvplot/tests/testgeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ def test_plot_with_specific_gv_tile_obj(self):
self.assertEqual(len(plot), 2)
self.assertIsInstance(plot.get(0), gv.element.WMTS)

def test_plot_with_features_properly_overlaid_underlaid(self):
# land should be under, borders should be over
plot = self.df.hvplot.points('x', 'y', features=["land", "borders"])
assert plot.get(0).group == "Land"
assert plot.get(2).group == "Borders"

class TestGeoElements(TestCase):

Expand Down
12 changes: 11 additions & 1 deletion hvplot/tests/testoperations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
from unittest import SkipTest
from parameterized import parameterized

import colorcet as cc
import holoviews as hv
import hvplot.pandas # noqa
import numpy as np
import pandas as pd

from holoviews import Store
from holoviews.element import Image, QuadMesh
from holoviews.element import Image, QuadMesh, ImageStack
from holoviews.element.comparison import ComparisonTestCase
from hvplot.converter import HoloViewsConverter
from packaging.version import Version


class TestDatashader(ComparisonTestCase):
Expand Down Expand Up @@ -194,6 +197,13 @@ def test_datashade_rescale_discrete_levels_default_True(self):
actual = plot.callback.inputs[0].callback.operation.p['rescale_discrete_levels']
assert actual is expected

def test_rasterize_by(self):
if Version(hv.__version__) < Version('1.18.0a1'):
raise SkipTest('hv.ImageStack introduced after 1.18.0a1')
expected = 'category'
plot = self.df.hvplot(x='x', y='y', by=expected, rasterize=True, dynamic=False)
assert isinstance(plot, ImageStack)
assert plot.opts["cmap"] == cc.palette['glasbey_category10']

class TestChart2D(ComparisonTestCase):

Expand Down
18 changes: 18 additions & 0 deletions hvplot/tests/testui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import re

import holoviews as hv
import hvplot.pandas

import pytest

from bokeh.sampledata import penguins
from hvplot.ui import hvDataFrameExplorer

Expand Down Expand Up @@ -85,3 +89,17 @@ def test_explorer_save(tmp_path):
explorer.save(outfile)

assert outfile.exists()


def test_explorer_kwargs_controls():
explorer = hvplot.explorer(df, title='Dummy title', width=200)

assert explorer.labels.title == 'Dummy title'
assert explorer.axes.width == 200


def test_explorer_kwargs_controls_error_not_supported():
with pytest.raises(
TypeError, match=re.escape("__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}")
):
hvplot.explorer(df, title='Dummy title', not_a_control_kwarg=None)
Loading

0 comments on commit 29174a7

Please sign in to comment.