Skip to content

Commit

Permalink
Improve handling of zero range datashader aggregations (#2842)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Jun 29, 2018
1 parent 0efabed commit 57cf92c
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 40 deletions.
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

0 comments on commit 57cf92c

Please sign in to comment.