Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds resample_when; the limit before automatically applying datashade/rasterize/downsample #1103

Merged
merged 17 commits into from
Oct 12, 2023
Merged
44 changes: 34 additions & 10 deletions hvplot/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
from holoviews.plotting.bokeh import OverlayPlot, colormap_generator
from holoviews.plotting.util import process_cmap
from holoviews.operation import histogram
from holoviews.operation import histogram, apply_when
from holoviews.streams import Buffer, Pipe
from holoviews.util.transform import dim
from packaging.version import Version
Expand Down Expand Up @@ -185,7 +185,7 @@ class HoloViewsConverter:
check_symmetric_max (default=1000000):
Size above which to stop checking for symmetry by default on the data.

Downsampling options
Resampling options
------------------
aggregator (default=None):
Aggregator to use when applying rasterize or datashade operation
Expand All @@ -212,6 +212,10 @@ class HoloViewsConverter:
Whether to apply rasterization using the Datashader library,
returning an aggregated Image (to be colormapped by the
plotting backend) instead of individual points
resample_when (default=None):
Applies a resampling operation (datashade, rasterize or downsample) if
the number of individual data points present in the current zoom range
is above this threshold. The raw plot is displayed otherwise.
x_sampling/y_sampling (default=None):
Specifies the smallest allowed sampling interval along the x/y axis.

Expand Down Expand Up @@ -286,7 +290,7 @@ class HoloViewsConverter:

_op_options = [
'datashade', 'rasterize', 'x_sampling', 'y_sampling',
'aggregator'
'downsample', 'aggregator', 'resample_when'
]

# Options specific to a particular plot type
Expand Down Expand Up @@ -383,9 +387,10 @@ def __init__(
logx=None, logy=None, loglog=None, hover=None, subplots=False,
label=None, invert=False, stacked=False, colorbar=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,
resample_when=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,
Expand Down Expand Up @@ -466,6 +471,12 @@ def __init__(
ylim = (y0, y1)

# Operations
if resample_when is not None and not any([rasterize, datashade, downsample]):
raise ValueError(
'At least one resampling operation (rasterize, datashader, '
'downsample) must be enabled when resample_when is set.'
)
self.resample_when = resample_when
self.datashade = datashade
self.rasterize = rasterize
self.downsample = downsample
Expand Down Expand Up @@ -1289,7 +1300,7 @@ def method_wrapper(ds, x, y):
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 = self._resample_obj(downsample1d, obj, opts)
layers = _transfer_opts_cur_backend(layers)
return layers

Expand Down Expand Up @@ -1353,7 +1364,7 @@ def method_wrapper(ds, x, y):
opts['cnorm'] = self._plot_opts['cnorm']
if 'rescale_discrete_levels' in self._plot_opts:
opts['rescale_discrete_levels'] = self._plot_opts['rescale_discrete_levels']
else:
elif self.rasterize:
operation = rasterize
if Version(hv.__version__) < Version('1.18.0a1'):
eltype = 'Image'
Expand All @@ -1364,8 +1375,7 @@ def method_wrapper(ds, x, y):
if self._dim_ranges.get('c', (None, None)) != (None, None):
style['clim'] = self._dim_ranges['c']

processed = operation(obj, **opts)

processed = self._resample_obj(operation, obj, opts)
if self.dynspread:
processed = dynspread(processed, max_px=self.kwds.get('max_px', 3),
threshold=self.kwds.get('threshold', 0.5))
Expand All @@ -1375,6 +1385,20 @@ def method_wrapper(ds, x, y):
layers = _transfer_opts_cur_backend(layers)
return layers

def _resample_obj(self, operation, obj, opts):
def exceeds_resample_when(plot):
return len(plot) > self.resample_when

if self.resample_when is not None:
processed = apply_when(
obj,
operation=partial(operation, **opts),
predicate=exceeds_resample_when
)
else:
processed = operation(obj, **opts)
return processed

def _get_opts(self, eltype, backend='bokeh', **custom):
opts = dict(self._plot_opts, **dict(self._style_opts, **self._norm_opts))
opts.update(custom)
Expand Down
56 changes: 54 additions & 2 deletions hvplot/tests/testoperations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
import hvplot.pandas # noqa
import numpy as np
import pandas as pd
import pytest

from holoviews import Store
from holoviews.element import Image, QuadMesh, ImageStack
from holoviews import Store, render
from holoviews.element import Image, QuadMesh, ImageStack, Points
from holoviews.core.spaces import DynamicMap
from holoviews.core.overlay import Overlay
from holoviews.element.chart import Scatter
from holoviews.element.comparison import ComparisonTestCase
from hvplot.converter import HoloViewsConverter
from packaging.version import Version
Expand Down Expand Up @@ -205,6 +209,54 @@ def test_rasterize_by(self):
assert isinstance(plot, ImageStack)
assert plot.opts["cmap"] == cc.palette['glasbey_category10']

def test_resample_when_error_unset_operation(self):
with pytest.raises(
ValueError,
match='At least one resampling operation'
):
self.df.hvplot(x='x', y='y', resample_when=10)

@parameterized.expand([('rasterize',), ('datashade',)])
def test_operation_resample_when(self, operation):
df = pd.DataFrame(
np.random.multivariate_normal((0, 0), [[0.1, 0.1], [0.1, 1.0]], (5000,))
).rename({0: "x", 1: "y"}, axis=1)
dmap = df.hvplot.scatter("x", "y", resample_when=1000, **{operation: True})
assert isinstance(dmap, DynamicMap)

render(dmap) # trigger dynamicmap
overlay = dmap.items()[0][1]
assert isinstance(overlay, Overlay)

image = overlay.get(0)
assert isinstance(image, Image)
assert len(image.data) > 0

scatter = overlay.get(1)
assert isinstance(scatter, Scatter)
assert len(scatter.data) == 0

@parameterized.expand([('points', Points), ('scatter', Scatter)])
def test_downsample_resample_when(self, kind, eltype):
df = pd.DataFrame(
np.random.multivariate_normal((0, 0), [[0.1, 0.1], [0.1, 1.0]], (5000,))
).rename({0: "x", 1: "y"}, axis=1)
dmap = df.hvplot(kind=kind, x="x", y="y", resample_when=1000, downsample=True)
assert isinstance(dmap, DynamicMap)

render(dmap) # trigger dynamicmap
overlay = dmap.items()[0][1]
assert isinstance(overlay, Overlay)

downsampled = overlay.get(0)
assert isinstance(downsampled, eltype)
assert len(downsampled) > 0

element = overlay.get(1)
assert isinstance(element, eltype)
assert len(element) == 0


class TestChart2D(ComparisonTestCase):

def setUp(self):
Expand Down