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

Improve handling of zero range datashader aggregations #2842

Merged
merged 9 commits into from
Jun 29, 2018
2 changes: 1 addition & 1 deletion holoviews/core/data/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ def range(cls, dataset, dimension):
expanded = cls.irregular(dataset, dimension)
column = cls.coords(dataset, dimension, expanded=expanded, edges=True)
else:
column = cls.values(dataset, dimension, flat=False)
column = cls.values(dataset, dimension, expanded=False, flat=False)
if column.dtype.kind == 'M':
dmin, dmax = column.min(), column.max()
if da and isinstance(column, da.Array):
Expand Down
5 changes: 4 additions & 1 deletion holoviews/core/data/xarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ def range(cls, dataset, dimension):
dmin, dmax = np.nanmin(data), np.nanmax(data)
else:
data = dataset.data[dim]
dmin, dmax = data.min().data, data.max().data
if len(data):
dmin, dmax = data.min().data, data.max().data
else:
dmin, dmax = np.NaN, np.NaN
if dask and isinstance(dmin, dask.array.Array):
dmin, dmax = dask.array.compute(dmin, dmax)
dmin = dmin if np.isscalar(dmin) else dmin.item()
Expand Down
100 changes: 62 additions & 38 deletions holoviews/operation/datashader.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,16 @@ def _get_sampling(self, element, x, y):
else:
x0, x1 = self.p.x_range
ex0, ex1 = element.range(x)
x_range = np.max([x0, ex0]), np.min([x1, ex1])
if x_range[0] == x_range[1]:
x_range = (x_range[0]-0.5, x_range[0]+0.5)
x_range = (np.min([np.max([x0, ex0]), ex1]),
np.max([np.min([x1, ex1]), ex0]))

if self.p.expand or not self.p.y_range:
y_range = self.p.y_range or element.range(y)
else:
y0, y1 = self.p.y_range
ey0, ey1 = element.range(y)
y_range = np.max([y0, ey0]), np.min([y1, ey1])
y_range = (np.min([np.max([y0, ey0]), ey1]),
np.max([np.min([y1, ey1]), ey0]))
width, height = self.p.width, self.p.height
(xstart, xend), (ystart, yend) = x_range, y_range

Expand All @@ -149,8 +149,6 @@ def _get_sampling(self, element, x, y):
xtype = 'datetime'
else:
xstart, xend = 0, 1
elif xstart == xend:
xstart, xend = (xstart-0.5, xend+0.5)
x_range = (xstart, xend)

ytype = 'numeric'
Expand All @@ -163,8 +161,6 @@ def _get_sampling(self, element, x, y):
ytype = 'datetime'
else:
ystart, yend = 0, 1
elif ystart == yend:
ystart, yend = (ystart-0.5, yend+0.5)
y_range = (ystart, yend)

# Compute highest allowed sampling density
Expand All @@ -174,8 +170,14 @@ def _get_sampling(self, element, x, y):
width = int(min([(xspan/self.p.x_sampling), width]))
if self.p.y_sampling:
height = int(min([(yspan/self.p.y_sampling), height]))
width, height = max([width, 1]), max([height, 1])
xunit, yunit = float(xspan)/width, float(yspan)/height
if xstart == xend or width == 0:
xunit, width = 0, 0
else:
xunit = float(xspan)/width
if ystart == yend or height == 0:
yunit, height = 0, 0
else:
yunit = float(yspan)/height
xs, ys = (np.linspace(xstart+xunit/2., xend-xunit/2., width),
np.linspace(ystart+yunit/2., yend-yunit/2., height))

Expand Down Expand Up @@ -443,18 +445,6 @@ def _process(self, element, key=None):
params = dict(get_param_values(element), kdims=[x, y],
datatype=['xarray'], bounds=bounds)

if x is None or y is None:
xarray = xr.DataArray(np.full((height, width), np.NaN, dtype=np.float32),
dims=['y', 'x'], coords={'x': xs, 'y': ys})
return self.p.element_type(xarray)
elif not len(data):
xarray = xr.DataArray(np.full((height, width), np.NaN, dtype=np.float32),
dims=[y.name, x.name], coords={x.name: xs, y.name: ys})
return self.p.element_type(xarray, **params)

cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)

column = agg_fn.column if agg_fn else None
if column:
dims = [d for d in element.dimensions('ranges') if d == column]
Expand All @@ -468,6 +458,27 @@ def _process(self, element, key=None):
vdims = Dimension('Count')
params['vdims'] = vdims

if x is None or y is None or width == 0 or height == 0:
xarray = xr.DataArray(np.full((height, width), np.NaN),
dims=['y', 'x'], coords={'x': xs, 'y': ys})
if width == 0:
params['xdensity'] = 1
if height == 0:
params['ydensity'] = 1
el = self.p.element_type(xarray, **params)
if isinstance(agg_fn, ds.count_cat):
vals = element.dimension_values(agg_fn.column, expanded=False)
dim = element.get_dimension(agg_fn.column)
return NdOverlay({v: el for v in vals}, dim)
return el
elif not len(data):
xarray = xr.DataArray(np.full((height, width), np.NaN),
dims=[y.name, x.name], coords={x.name: xs, y.name: ys})
return self.p.element_type(xarray, **params)

cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)

dfdata = PandasInterface.as_dframe(data)
agg = getattr(cvs, glyph)(dfdata, x.name, y.name, agg_fn)
if 'x_axis' in agg.coords and 'y_axis' in agg.coords:
Expand Down Expand Up @@ -583,7 +594,19 @@ def _process(self, element, key=None):
exspan, eyspan = (x1-x0), (y1-y0)
width = min([int((xspan/exspan) * len(coords[0])), width])
height = min([int((yspan/eyspan) * len(coords[1])), height])
width, height = max([width, 1]), max([height, 1])

# Compute bounds (converting datetimes)
if xtype == 'datetime':
xstart, xend = (np.array([xstart, xend])/10e5).astype('datetime64[us]')
if ytype == 'datetime':
ystart, yend = (np.array([ystart, yend])/10e5).astype('datetime64[us]')
bbox = BoundingBox(points=[(xstart, ystart), (xend, yend)])

params = dict(bounds=bbox)
if width == 0 or height == 0:
if width == 0: params['xdensity'] = 1
if height == 0: params['ydensity'] = 1
return element.clone([], **params)

cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)
Expand All @@ -604,14 +627,7 @@ def _process(self, element, key=None):
regridded[vd] = rarray
regridded = xr.Dataset(regridded)

# Compute bounds (converting datetimes)
if xtype == 'datetime':
xstart, xend = (np.array([xstart, xend])/10e5).astype('datetime64[us]')
if ytype == 'datetime':
ystart, yend = (np.array([ystart, yend])/10e5).astype('datetime64[us]')
bbox = BoundingBox(points=[(xstart, ystart), (xend, yend)])
return element.clone(regridded, bounds=bbox,
datatype=['xarray']+element.datatype)
return element.clone(regridded, bounds=bbox, datatype=['xarray']+element.datatype)



Expand Down Expand Up @@ -669,9 +685,7 @@ def _process(self, element, key=None):
else:
x, y = element.kdims
info = self._get_sampling(element, x, y)
(x_range, y_range), _, (width, height), (xtype, ytype) = info
cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)
(x_range, y_range), (xs, ys), (width, height), (xtype, ytype) = info

agg = self.p.aggregator
if not (element.vdims or element.nodes.vdims):
Expand All @@ -685,18 +699,28 @@ def _process(self, element, key=None):
precomputed = self._precomputed[element._plot_id]
else:
precomputed = self._precompute(element)

vdim = element.vdims[0] if element.vdims else element.nodes.vdims[0]
params = dict(get_param_values(element), kdims=[x, y],
datatype=['xarray'], vdims=[vdim])

if width == 0 or height == 0:
if width == 0: params['xdensity'] = 1
if height == 0: params['ydensity'] = 1
bounds = (x_range[0], y_range[0], x_range[1], y_range[1])
return Image((xs, ys, np.zeros((height, width))), bounds=bounds, **params)

simplices = precomputed['simplices']
pts = precomputed['vertices']
mesh = precomputed['mesh']
if self.p.precompute:
self._precomputed = {element._plot_id: precomputed}

vdim = element.vdims[0] if element.vdims else element.nodes.vdims[0]
cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)
interpolate = bool(self.p.interpolation)
agg = cvs.trimesh(pts, simplices, agg=self._get_aggregator(element),
interp=interpolate, mesh=mesh)
params = dict(get_param_values(element), kdims=[x, y],
datatype=['xarray'], vdims=[vdim])
return Image(agg, **params)


Expand Down Expand Up @@ -736,7 +760,7 @@ class rasterize(AggregationOperation):
dimensions of the linked plot and the ranges of the axes.
"""

aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
aggregator = param.ClassSelector(class_=(ds.reductions.Reduction, basestring),
default=None)

interpolation = param.ObjectSelector(default='bilinear',
Expand Down
3 changes: 3 additions & 0 deletions holoviews/plotting/bokeh/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def get_data(self, element, ranges, style):
b, t = t, b
dh, dw = t-b, r-l

if 0 in img.shape:
img = np.array([[np.NaN]])

data = dict(image=[img], x=[l], y=[b], dw=[dw], dh=[dh])
return (data, mapping, style)

Expand Down
10 changes: 10 additions & 0 deletions tests/core/data/testxarrayinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def test_concat_grid_3d_shape_mismatch(self):
ds = Dataset(([1, 2], [0, 1, 2], [1, 2, 3], arr), ['Default', 'x', 'y'], 'z')
self.assertEqual(concat(hmap), ds)

def test_zero_sized_coordinates_range(self):
da = xr.DataArray(np.empty((2, 0)), dims=('y', 'x'), coords={'x': [], 'y': [0 ,1]}, name='A')
ds = Dataset(da)
x0, x1 = ds.range('x')
self.assertTrue(np.isnan(x0))
self.assertTrue(np.isnan(x1))
z0, z1 = ds.range('A')
self.assertTrue(np.isnan(z0))
self.assertTrue(np.isnan(z1))

def test_dataset_array_init_hm(self):
"Tests support for arrays (homogeneous)"
raise SkipTest("Not supported")
Expand Down
36 changes: 36 additions & 0 deletions tests/operation/testdatashader.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ def test_aggregate_points(self):
vdims=['Count'])
self.assertEqual(img, expected)

def test_aggregate_zero_range_points(self):
p = Points([(0, 0), (1, 1)])
agg = rasterize(p, x_range=(0, 0), y_range=(0, 1), expand=False, dynamic=False)
img = Image(([], [0.25, 0.75], np.zeros((2, 0))), bounds=(0, 0, 0, 1), xdensity=1, vdims=['Count'])
self.assertEqual(agg, img)

def test_aggregate_points_target(self):
points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
Expand All @@ -57,6 +63,18 @@ def test_aggregate_points_categorical(self):
kdims=['z'])
self.assertEqual(img, expected)

def test_aggregate_points_categorical_zero_range(self):
points = Points([(0.2, 0.3, 'A'), (0.4, 0.7, 'B'), (0, 0.99, 'C')], vdims='z')
img = aggregate(points, dynamic=False, x_range=(0, 0), y_range=(0, 1),
aggregator=ds.count_cat('z'))
xs, ys = [], [0.25, 0.75]
params = dict(bounds=(0, 0, 0, 1), xdensity=1)
expected = NdOverlay({'A': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params),
'B': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params),
'C': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params)},
kdims=['z'])
self.assertEqual(img, expected)

def test_aggregate_curve(self):
curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)])
expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]),
Expand Down Expand Up @@ -273,6 +291,15 @@ def test_rasterize_trimesh_no_vdims(self):
bounds=(0, 0, 1, 1), vdims='Count')
self.assertEqual(img, image)

def test_rasterize_trimesh_no_vdims_zero_range(self):
simplices = [(0, 1, 2), (3, 2, 1)]
vertices = [(0., 0.), (0., 1.), (1., 0), (1, 1)]
trimesh = TriMesh((simplices, vertices))
img = rasterize(trimesh, height=2, x_range=(0, 0), dynamic=False)
image = Image(([], [0.25, 0.75], np.zeros((2, 0))),
bounds=(0, 0, 0, 1), xdensity=1, vdims='Count')
self.assertEqual(img, image)

def test_rasterize_trimesh(self):
simplices = [(0, 1, 2, 0.5), (3, 2, 1, 1.5)]
vertices = [(0., 0.), (0., 1.), (1., 0), (1, 1)]
Expand All @@ -282,6 +309,15 @@ def test_rasterize_trimesh(self):
bounds=(0, 0, 1, 1))
self.assertEqual(img, image)

def test_rasterize_trimesh_zero_range(self):
simplices = [(0, 1, 2, 0.5), (3, 2, 1, 1.5)]
vertices = [(0., 0.), (0., 1.), (1., 0), (1, 1)]
trimesh = TriMesh((simplices, vertices), vdims=['z'])
img = rasterize(trimesh, x_range=(0, 0), height=2, dynamic=False)
image = Image(([], [0.25, 0.75], np.zeros((2, 0))),
bounds=(0, 0, 0, 1), xdensity=1)
self.assertEqual(img, image)

def test_rasterize_trimesh_vertex_vdims(self):
simplices = [(0, 1, 2), (3, 2, 1)]
vertices = [(0., 0., 1), (0., 1., 2), (1., 0., 3), (1., 1., 4)]
Expand Down