Skip to content

Commit

Permalink
Added option for dimension range padding (#2293)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Sep 13, 2018
1 parent 04ee4e9 commit a1b9cde
Show file tree
Hide file tree
Showing 48 changed files with 1,816 additions and 248 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ install:
- conda info -a
- conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION flake8 scipy=1.0.0 numpy freetype nose pandas=0.22.0 jupyter ipython=5.4.1 param matplotlib=2.1.2 xarray networkx
- source activate test-environment
- conda install -c conda-forge filelock iris plotly=2.7 flexx ffmpeg netcdf4=1.3.1 --quiet
- conda install -c conda-forge filelock iris plotly=2.7 flexx=0.4.1 ffmpeg netcdf4=1.3.1 --quiet
- conda install -c bokeh datashader dask bokeh=0.12.15 selenium
- if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then
conda install python=3.4.3;
Expand Down
26 changes: 18 additions & 8 deletions holoviews/core/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import param

from ..dimension import redim
from ..util import dimension_range, unique_iterator
from ..util import unique_iterator
from .interface import Interface, iloc, ndloc
from .array import ArrayInterface
from .dictionary import DictInterface
Expand Down Expand Up @@ -274,22 +274,32 @@ def sort(self, by=[], reverse=False):
return self.clone(sorted_columns)


def range(self, dim, data_range=True):
def range(self, dim, data_range=True, dimension_range=True):
"""
Computes the range of values along a supplied dimension, taking
into account the range and soft_range defined on the Dimension
object.
Returns the range of values along the specified dimension.
dimension: str/int/Dimension
The dimension to compute the range on.
data_range: bool
Whether the range should include the data range or only
the dimension ranges
dimension_range:
Whether to compute the range including the Dimension range
and soft_range
"""
dim = self.get_dimension(dim)
if dim is None:

if dim is None or (not data_range and not dimension_range):
return (None, None)
elif all(util.isfinite(v) for v in dim.range):
elif all(util.isfinite(v) for v in dim.range) and dimension_range:
return dim.range
elif dim in self.dimensions() and data_range and len(self):
lower, upper = self.interface.range(self, dim)
else:
lower, upper = (np.NaN, np.NaN)
return dimension_range(lower, upper, dim)
if not dimension_range:
return lower, upper
return util.dimension_range(lower, upper, dim.range, dim.soft_range)


def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs):
Expand Down
24 changes: 16 additions & 8 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
import numpy as np
import param

from ..core import util
from ..core.util import (basestring, sanitize_identifier, isfinite,
group_sanitizer, label_sanitizer, max_range,
find_range, dimension_sanitizer, OrderedDict,
bytes_to_unicode, unicode, dt64_to_dt, unique_array,
builtins, config, dimension_range, disable_constant)
builtins, config, disable_constant)
from .options import Store, StoreOptions
from .pprint import PrettyPrinter

Expand Down Expand Up @@ -1104,18 +1105,23 @@ def dimension_values(self, dimension, expanded=True, flat=True):
(dimension, self.__class__.__name__))


def range(self, dimension, data_range=True):
def range(self, dimension, data_range=True, dimension_range=True):
"""
Returns the range of values along the specified dimension.
If data_range is True, the data may be used to try and infer
the appropriate range. Otherwise, (None,None) is returned to
indicate that no range is defined.
dimension: str or int or Dimension
The dimension to compute the range on.
data_range: bool (optional)
Whether the range should include the data range or only
the dimension ranges
dimension_range: bool (optional)
Whether to compute the range including the Dimension range
and soft_range.
"""
dimension = self.get_dimension(dimension)
if dimension is None:
if dimension is None or (not data_range and not dimension_range):
return (None, None)
elif all(isfinite(v) for v in dimension.range):
elif all(isfinite(v) for v in dimension.range) and dimension_range:
return dimension.range
elif data_range:
if dimension in self.kdims+self.vdims:
Expand All @@ -1129,7 +1135,9 @@ def range(self, dimension, data_range=True):
lower, upper = max_range(ranges)
else:
lower, upper = (np.NaN, np.NaN)
return dimension_range(lower, upper, dimension)
if not dimension_range:
return lower, upper
return util.dimension_range(lower, upper, dimension.range, dimension.soft_range)


def __repr__(self):
Expand Down
4 changes: 2 additions & 2 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,11 +845,11 @@ def _validate_key(self, key):
for ind, val in enumerate(key):
kdim = self.kdims[ind]
low, high = util.max_range([kdim.range, kdim.soft_range])
if low is not np.NaN:
if util.is_number(low) and util.isfinite(low):
if val < low:
raise KeyError("Key value %s below lower bound %s"
% (val, low))
if high is not np.NaN:
if util.is_number(high) and util.isfinite(high):
if val > high:
raise KeyError("Key value %s above upper bound %s"
% (val, high))
Expand Down
72 changes: 58 additions & 14 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,36 +810,80 @@ def find_range(values, soft_range=[]):
return (None, None)


def max_range(ranges):
def is_finite(value):
"""
Safe check whether a value is finite, only None, NaN and inf
values are considered non-finite and allows checking all types not
restricted to numeric types.
"""
if value is None:
return False
try:
return np.isfinite(value)
except:
return True


def max_range(ranges, combined=True):
"""
Computes the maximal lower and upper bounds from a list bounds.
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered')
values = [r for r in ranges for v in r if v is not None]
values = [tuple(np.NaN if v is None else v for v in r) for r in ranges]
if pd and all(isinstance(v, pd.Timestamp) for r in values for v in r):
values = [(v1.to_datetime64(), v2.to_datetime64()) for v1, v2 in values]
arr = np.array(values)
if arr.dtype.kind in 'OSU':
arr = list(python2sort([v for v in arr.flat if not is_nan(v) and v is not None]))
if not len(arr):
return np.NaN, np.NaN
elif arr.dtype.kind in 'OSU':
arr = list(python2sort([v for r in values for v in r if not is_nan(v) and v is not None]))
return arr[0], arr[-1]
if arr.dtype.kind in 'M':
return arr[:, 0].min(), arr[:, 1].max()
return (np.nanmin(arr[:, 0]), np.nanmax(arr[:, 1]))
elif arr.dtype.kind in 'M':
return (arr.min(), arr.max()) if combined else (arr[:, 0].min(), arr[:, 1].min())
if combined:
return (np.nanmin(arr), np.nanmax(arr))
else:
return (np.nanmin(arr[:, 0]), np.nanmax(arr[:, 1]))
except:
return (np.NaN, np.NaN)


def dimension_range(lower, upper, dimension):
def range_pad(lower, upper, padding=None, log=False):
"""
Pads the range by a fraction of the interval
"""
if padding is not None and not isinstance(padding, tuple):
padding = (padding, padding)
if is_number(lower) and is_number(upper) and padding is not None:
if not isinstance(lower, datetime_types) and log and lower > 0 and upper > 0:
log_min = np.log(lower) / np.log(10)
log_max = np.log(upper) / np.log(10)
lspan = (log_max-log_min)*(1+padding[0]*2)
uspan = (log_max-log_min)*(1+padding[1]*2)
center = (log_min+log_max) / 2.0
start, end = np.power(10, center-lspan/2.), np.power(10, center+uspan/2.)
else:
span = (upper-lower)
lpad = span*(padding[0])
upad = span*(padding[1])
start, end = lower-lpad, upper+upad
else:
start, end = lower, upper
return start, end


def dimension_range(lower, upper, hard_range, soft_range, padding=None, log=False):
"""
Computes the range along a dimension by combining the data range
with the Dimension soft_range and range.
"""
lower, upper = max_range([(lower, upper), dimension.soft_range])
dmin, dmax = dimension.range
lower = dmin if isfinite(dmin) else lower
upper = dmax if isfinite(dmax) else upper
lower, upper = range_pad(lower, upper, padding, log)
lower, upper = max_range([(lower, upper), soft_range])
dmin, dmax = hard_range
lower = lower if dmin is None or not np.isfinite(dmin) else dmin
upper = upper if dmax is None or not np.isfinite(dmax) else dmax
return lower, upper


Expand Down Expand Up @@ -1035,6 +1079,7 @@ def dimension_sort(odict, kdims, vdims, key_index):
# Copied from param should make param version public
def is_number(obj):
if isinstance(obj, numbers.Number): return True
elif isinstance(obj, (np.str_, np.unicode_)): return False
# The extra check is for classes that behave like numbers, such as those
# found in numpy, gmpy, etc.
elif (hasattr(obj, '__int__') and hasattr(obj, '__add__')): return True
Expand Down Expand Up @@ -1671,8 +1716,7 @@ def validate_regular_sampling(values, rtol=10e-6):
Returns a boolean indicating whether the sampling is regular.
"""
diffs = np.diff(values)
vals = np.unique(diffs)
return not (len(vals) > 1 and np.abs(vals.min()-vals.max()) > abs(diffs.min()*rtol))
return (len(diffs) < 1) or abs(diffs.min()-diffs.max()) < abs(diffs.min()*rtol)


def compute_density(start, end, length, time_unit='us'):
Expand Down
6 changes: 4 additions & 2 deletions holoviews/element/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class ErrorBars(Chart):
bounds=(1, 3), constant=True)


def range(self, dim, data_range=True):
def range(self, dim, data_range=True, dimension_range=True):
didx = self.get_dimension_index(dim)
dim = self.get_dimension(dim)
if didx == 1 and data_range and len(self):
Expand All @@ -103,7 +103,9 @@ def range(self, dim, data_range=True):
pos_error = neg_error
lower = np.nanmin(mean-neg_error)
upper = np.nanmax(mean+pos_error)
return util.dimension_range(lower, upper, dim)
if not dimension_range:
return (lower, upper)
return util.dimension_range(lower, upper, dim.range, dim.soft_range)
return super(ErrorBars, self).range(dim, data_range)


Expand Down
8 changes: 4 additions & 4 deletions holoviews/element/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,14 @@ def _split_edgepaths(self):
return self.edgepaths.clone(split_path(self.edgepaths))


def range(self, dimension, data_range=True):
def range(self, dimension, data_range=True, dimension_range=True):
if self.nodes and dimension in self.nodes.dimensions():
node_range = self.nodes.range(dimension, data_range)
node_range = self.nodes.range(dimension, data_range, dimension_range)
if self._edgepaths:
path_range = self._edgepaths.range(dimension, data_range)
path_range = self._edgepaths.range(dimension, data_range, dimension_range)
return max_range([node_range, path_range])
return node_range
return super(Graph, self).range(dimension, data_range)
return super(Graph, self).range(dimension, data_range, dimension_range)


def dimensions(self, selection='all', label=False):
Expand Down
23 changes: 12 additions & 11 deletions holoviews/element/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from ..core import Dimension, Element2D, Overlay, Dataset
from ..core.boundingregion import BoundingRegion, BoundingBox
from ..core.sheetcoords import SheetCoordinateSystem, Slice
from ..core.util import dimension_range, compute_density, datetime_types
from .chart import Curve
from .graphs import TriMesh
from .tabular import Table
Expand Down Expand Up @@ -70,13 +69,15 @@ def __getitem__(self, slices):
extents=None)


def range(self, dim, data_range=True):
def range(self, dim, data_range=True, dimension_range=True):
idx = self.get_dimension_index(dim)
if data_range and idx == 2:
dimension = self.get_dimension(dim)
lower, upper = np.nanmin(self.data), np.nanmax(self.data)
return dimension_range(lower, upper, dimension)
return super(Raster, self).range(dim, data_range)
if not dimension_range:
return lower, upper
return util.dimension_range(lower, upper, dimension.range, dimension.soft_range)
return super(Raster, self).range(dim, data_range, dimension_range)


def dimension_values(self, dim, expanded=True, flat=True):
Expand Down Expand Up @@ -277,8 +278,8 @@ def __init__(self, data, kdims=None, vdims=None, bounds=None, extents=None,
if self.interface is ImageInterface and not isinstance(data, np.ndarray):
data_bounds = self.bounds.lbrt()
l, b, r, t = bounds.lbrt()
xdensity = xdensity if xdensity else compute_density(l, r, dim1, self._time_unit)
ydensity = ydensity if ydensity else compute_density(b, t, dim2, self._time_unit)
xdensity = xdensity if xdensity else util.compute_density(l, r, dim1, self._time_unit)
ydensity = ydensity if ydensity else util.compute_density(b, t, dim2, self._time_unit)
if not np.isfinite(xdensity) or not np.isfinite(ydensity):
raise ValueError('Density along Image axes could not be determined. '
'If the data contains only one coordinate along the '
Expand Down Expand Up @@ -329,9 +330,9 @@ def _validate(self, data_bounds, supplied_bounds):

not_close = False
for r, c in zip(bounds, self.bounds.lbrt()):
if isinstance(r, datetime_types):
if isinstance(r, util.datetime_types):
r = util.dt_to_int(r)
if isinstance(c, datetime_types):
if isinstance(c, util.datetime_types):
c = util.dt_to_int(c)
if util.isfinite(r) and not np.isclose(r, c, rtol=self.rtol):
not_close = True
Expand Down Expand Up @@ -521,7 +522,7 @@ def closest(self, coords=[], **kwargs):
return [getter(self.closest_cell_center(*el)) for el in coords]


def range(self, dim, data_range=True):
def range(self, dim, data_range=True, dimension_range=True):
idx = self.get_dimension_index(dim)
dimension = self.get_dimension(dim)
if idx in [0, 1] and data_range and dimension.range == (None, None):
Expand All @@ -531,11 +532,11 @@ def range(self, dim, data_range=True):
low, high = super(Image, self).range(dim, data_range)
density = self.ydensity if idx else self.xdensity
halfd = (1./density)/2.
if isinstance(low, datetime_types):
if isinstance(low, util.datetime_types):
halfd = np.timedelta64(int(round(halfd)), self._time_unit)
return (low-halfd, high+halfd)
else:
return super(Image, self).range(dim, data_range)
return super(Image, self).range(dim, data_range, dimension_range)


def table(self, datatype=None):
Expand Down
4 changes: 2 additions & 2 deletions holoviews/element/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def __init__(self, data, kdims=None, vdims=None, **params):
self.vdims = process_dimensions(None, vdims)['vdims']


def range(self, dim, data_range=True):
def range(self, dim, data_range=True, dimension_range=True):
iskdim = self.get_dimension(dim) not in self.vdims
return super(StatisticsElement, self).range(dim, data_range=iskdim)
return super(StatisticsElement, self).range(dim, iskdim, dimension_range)


def dimension_values(self, dim, expanded=True, flat=True):
Expand Down
6 changes: 3 additions & 3 deletions holoviews/plotting/bokeh/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_batched_data(self, element, ranges=None):
data[k].extend(eld)
return data, elmapping, style

def get_extents(self, element, ranges=None):
def get_extents(self, element, ranges=None, range_type='combined'):
return None, None, None, None


Expand Down Expand Up @@ -139,7 +139,7 @@ def _init_glyph(self, plot, mapping, properties):
plot.renderers.append(box)
return None, box

def get_extents(self, element, ranges=None):
def get_extents(self, element, ranges=None, range_type='combined'):
return None, None, None, None


Expand Down Expand Up @@ -241,7 +241,7 @@ def _init_glyph(self, plot, mapping, properties, key):
plot.renderers.append(glyph)
return None, glyph

def get_extents(self, element, ranges=None):
def get_extents(self, element, ranges=None, range_type='combined'):
return None, None, None, None


Expand Down
Loading

0 comments on commit a1b9cde

Please sign in to comment.