diff --git a/.travis.yml b/.travis.yml index d5502f0613..34d695bc95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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; diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 30b5944aa9..134e8930cc 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -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 @@ -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): diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 3a90c1b9d1..f6d22712c8 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -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 @@ -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: @@ -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): diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index db5a101941..9f8b9fe6ac 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -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)) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 6f769585bf..4cbb101f19 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -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 @@ -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 @@ -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'): diff --git a/holoviews/element/chart.py b/holoviews/element/chart.py index 45eaba3fd7..7c59e7fbfc 100644 --- a/holoviews/element/chart.py +++ b/holoviews/element/chart.py @@ -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): @@ -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) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index e82fab19a1..a36a0eca61 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -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): diff --git a/holoviews/element/raster.py b/holoviews/element/raster.py index 63e2c8945f..090015e81f 100644 --- a/holoviews/element/raster.py +++ b/holoviews/element/raster.py @@ -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 @@ -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): @@ -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 ' @@ -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 @@ -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): @@ -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): diff --git a/holoviews/element/stats.py b/holoviews/element/stats.py index 383b219030..8741156cd5 100644 --- a/holoviews/element/stats.py +++ b/holoviews/element/stats.py @@ -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): diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 03674316ae..1b13ce5620 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -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 @@ -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 @@ -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 diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 984f3cd7a2..1f7221f3e0 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -7,10 +7,10 @@ from bokeh.transform import jitter from ...core import Dataset, OrderedDict -from ...core.util import max_range, basestring, dimension_sanitizer, isfinite +from ...core.util import max_range, basestring, dimension_sanitizer, isfinite, range_pad from ...element import Bars from ...operation import interpolate_curve -from ..util import compute_sizes, get_min_distance, dim_axis_label +from ..util import compute_sizes, get_min_distance, dim_axis_label, get_axis_padding from .element import (ElementPlot, ColorbarPlot, LegendPlot, line_properties, fill_properties) from .util import expand_batched_style, categorize_array, rgb2hex, mpl_to_bokeh @@ -169,7 +169,7 @@ def _get_lengths(self, element, ranges): (x0, x1), (y0, y1) = (element.range(i) for i in range(2)) if mag_dim: magnitudes = element.dimension_values(mag_dim) - _, max_magnitude = ranges[mag_dim.name] + _, max_magnitude = ranges[mag_dim.name]['combined'] if self.normalize_lengths and max_magnitude != 0: magnitudes = magnitudes / max_magnitude if self.rescale_lengths: @@ -341,12 +341,13 @@ def get_data(self, element, ranges, style): self._get_hover_data(data, element) return (data, mapping, style) - def get_extents(self, element, ranges): - x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(element, ranges) - ylow, yhigh = element.get_dimension(1).range - y0 = ylow if isfinite(ylow) else np.nanmin([0, y0]) - y1 = yhigh if isfinite(yhigh) else np.nanmax([0, y1]) - return (x0, y0, x1, y1) + def get_extents(self, element, ranges, range_type='combined'): + ydim = element.get_dimension(1) + s0, s1 = ranges[ydim.name]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s0 = max(s1, 0) if isfinite(s1) else 0 + ranges[ydim.name]['soft'] = (s0, s1) + return super(HistogramPlot, self).get_extents(element, ranges, range_type) @@ -529,16 +530,20 @@ class AreaPlot(SpreadPlot): _stream_data = False # Plot does not support streaming data - def get_extents(self, element, ranges): - vdims = element.vdims + def get_extents(self, element, ranges, range_type='combined'): + vdims = element.vdims[:2] vdim = vdims[0].name if len(vdims) > 1: - ranges[vdim] = max_range([ranges[vd.name] for vd in vdims]) + new_range = {} + for r in ranges[vdim]: + new_range[r] = max_range([ranges[vd.name][r] for vd in vdims]) + ranges[vdim] = new_range else: - vdim = vdims[0].name - ranges[vdim] = (np.nanmin([0, ranges[vdim][0]]), ranges[vdim][1]) - return super(AreaPlot, self).get_extents(element, ranges) - + s0, s1 = ranges[vdim]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s1 = max(s1, 0) if isfinite(s1) else 0 + ranges[vdim]['soft'] = (s0, s1) + return super(AreaPlot, self).get_extents(element, ranges, range_type) def get_data(self, element, ranges, style): mapping = dict(x='x', y='y') @@ -578,9 +583,15 @@ class SpikesPlot(ColorbarPlot): _plot_methods = dict(single='segment') - def get_extents(self, element, ranges): - l, b, r, t = super(SpikesPlot, self).get_extents(element, ranges) - if len(element.dimensions()) == 1: + def get_extents(self, element, ranges, range_type='combined'): + if len(element.dimensions()) > 1: + ydim = element.get_dimension(1) + s0, s1 = ranges[ydim.name]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s0 = max(s1, 0) if isfinite(s1) else 0 + ranges[ydim.name]['soft'] = (s0, s1) + l, b, r, t = super(SpikesPlot, self).get_extents(element, ranges, range_type) + if len(element.dimensions()) == 1 and range_type != 'hard': if self.batched: bs, ts = [], [] # Iterate over current NdOverlay and compute extents @@ -592,13 +603,9 @@ def get_extents(self, element, ranges): length = opts.get('spike_length', self.spike_length) bs.append(pos) ts.append(pos+length) - b = np.nanmin(bs) - t = np.nanmax(ts) + b, t = (np.nanmin(bs), np.nanmax(ts)) else: b, t = self.position, self.position+self.spike_length - else: - b = np.nanmin([0, b]) - t = np.nanmax([0, t]) return l, b, r, t def get_data(self, element, ranges, style): @@ -689,7 +696,7 @@ class BarPlot(ColorbarPlot, LegendPlot): # Declare that y-range should auto-range if not bounded _y_range_type = Range1d - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): """ Make adjustments to plot extents by computing stacked bar heights, adjusting the bar baseline @@ -700,10 +707,10 @@ def get_extents(self, element, ranges): element = Bars(overlay.table(), kdims=element.kdims+overlay.kdims, vdims=element.vdims) for kd in overlay.kdims: - ranges[kd.name] = overlay.range(kd) + ranges[kd.name]['combined'] = overlay.range(kd) stacked = element.get_dimension(self.stack_index) - extents = super(BarPlot, self).get_extents(element, ranges) + extents = super(BarPlot, self).get_extents(element, ranges, range_type) xdim = element.kdims[0] ydim = element.vdims[0] @@ -714,7 +721,11 @@ def get_extents(self, element, ranges): neg_range = ds.select(**{ydim.name: (None, 0)}).aggregate(xdim, function=np.sum).range(ydim) y0, y1 = max_range([pos_range, neg_range]) else: - y0, y1 = ranges[ydim.name] + y0, y1 = ranges[ydim.name]['combined'] + + padding = 0 if self.overlaid else self.padding + _, ypad, _ = get_axis_padding(padding) + y0, y1 = range_pad(y0, y1, ypad, self.logy) # Set y-baseline if y0 < 0: @@ -856,7 +867,7 @@ def get_data(self, element, ranges, style): container_type=OrderedDict, datatype=['dataframe', 'dictionary']) - y0, y1 = ranges.get(ydim.name, (None, None)) + y0, y1 = ranges.get(ydim.name, {'combined': (None, None)})['combined'] if self.logy: bottom = (ydim.range[0] or (10**(np.log10(y1)-2)) if y1 else 0.01) else: diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 814a0cdba6..b1c8f15cf3 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -495,6 +495,7 @@ def _update_ranges(self, element, ranges): or xfactors is not None) yupdate = ((not self.model_changed(y_range) and (framewise or streaming)) or yfactors is not None) + if not self.drawn or xupdate: self._update_range(x_range, l, r, xfactors, self.invert_xaxis, self._shared['x'], self.logx, streaming) @@ -518,7 +519,7 @@ def _update_range(self, axis_range, low, high, factors, invert, shared, log, str if shared: shared = (axis_range.start, axis_range.end) low, high = util.max_range([(low, high), shared]) - if log and (low is None or low <= 0): + if not isinstance(low, util.datetime_types) and log and (low is None or low <= 0): low = 0.01 if high < 0.01 else 10**(np.log10(high)-2) self.warning("Logarithmic axis range encountered value less than or equal to zero, " "please supply explicit lower-bound to override default of %.3f." % low) @@ -559,6 +560,14 @@ def _categorize_data(self, data, cols, dims): (isinstance(column, list) or column.dtype.kind not in 'SU')): data[col] = [dims[i].pprint_value(v) for v in column] + + def get_aspect(self, xspan, yspan): + """ + Computes the aspect ratio of the plot + """ + return self.width/self.height + + def _get_factors(self, element): """ Get factors for categorical axes. @@ -716,7 +725,6 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): self.current_key = key style_element = element.last if self.batched else element ranges = util.match_spec(style_element, ranges) - # Initialize plot, source and glyph if plot is None: plot = self._init_plot(key, style_element, ranges=ranges, plots=plots) @@ -1066,7 +1074,7 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=Non ncolors = None if factors is None else len(factors) if dim: if dim.name in ranges: - low, high = ranges.get(dim.name) + low, high = ranges[dim.name]['combined'] else: low, high = element.range(dim.name) if self.symmetric: @@ -1245,8 +1253,8 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot): 'yticks', 'xrotation', 'yrotation', 'lod', 'border', 'invert_xaxis', 'invert_yaxis', 'sizing_mode', 'title_format', 'legend_position', 'legend_offset', - 'legend_cols', 'gridstyle', 'legend_muted'] - + 'legend_cols', 'gridstyle', 'legend_muted', 'padding', + 'xlim', 'ylim', 'zlim'] def _process_legend(self): plot = self.handles['plot'] diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index cb88580b5c..e1bed58485 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -73,11 +73,8 @@ def _hover_opts(self, element): dims = [] return dims, {} - def get_extents(self, element, ranges): - xdim, ydim = element.nodes.kdims[:2] - x0, x1 = ranges[xdim.name] - y0, y1 = ranges[ydim.name] - return (x0, y0, x1, y1) + def get_extents(self, element, ranges, range_type=True): + return super(GraphPlot, self).get_extents(element.nodes, ranges, range_type) def _get_axis_labels(self, *args, **kwargs): """ @@ -317,11 +314,13 @@ class ChordPlot(GraphPlot): _draw_order = ['scatter', 'multi_line', 'layout'] - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): """ A Chord plot is always drawn on a unit circle. """ xdim, ydim = element.nodes.kdims[:2] + if range_type not in ('combined', 'data', 'extents'): + return xdim.range[0], ydim.range[0], xdim.range[1], ydim.range[1] rng = 1.1 if element.nodes.get_dimension(self.label_index) is None else 1.4 x0, x1 = max_range([xdim.range, (-rng, rng)]) y0, y1 = max_range([ydim.range, (-rng, rng)]) diff --git a/holoviews/plotting/bokeh/heatmap.py b/holoviews/plotting/bokeh/heatmap.py index 523d6ffa85..eb33542db1 100644 --- a/holoviews/plotting/bokeh/heatmap.py +++ b/holoviews/plotting/bokeh/heatmap.py @@ -279,12 +279,12 @@ def _postprocess_hover(self, renderer, source): super(RadialHeatMapPlot, self)._postprocess_hover(renderer, source) - def get_extents(self, view, ranges): + def get_extents(self, view, ranges, range_type='combined'): """Supply custom, static extents because radial heatmaps always have the same boundaries. - """ - + if range_type not in ('data', 'combined'): + return (None,)*4 lower = -self.radius_outer upper = 2 * self.max_radius + self.radius_outer return (lower, lower, upper, upper) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index d1aa53de66..3969a56ea8 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -151,7 +151,7 @@ def get_data(self, element, ranges, style): return data, mapping, style q, r = (element.dimension_values(i) for i in range(2)) x, y = element.kdims[::-1] if self.invert_axes else element.kdims - (x0, x1), (y0, y1) = ranges[x.name], ranges[y.name] + (x0, x1), (y0, y1) = ranges[x.name]['combined'], ranges[y.name]['combined'] if isinstance(self.gridsize, tuple): sx, sy = self.gridsize else: diff --git a/holoviews/plotting/bokeh/sankey.py b/holoviews/plotting/bokeh/sankey.py index 7620b88787..475e9d1400 100644 --- a/holoviews/plotting/bokeh/sankey.py +++ b/holoviews/plotting/bokeh/sankey.py @@ -136,11 +136,13 @@ def _patch_hover(self, element, data): data['patches_1'][src] = [lookup.get(v, v) for v in src_vals] data['patches_1'][tgt] = [lookup.get(v, v) for v in tgt_vals] - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): + if range_type == 'extents': + return element.nodes.extents xdim, ydim = element.nodes.kdims[:2] xpad = .05 if self.label_index is None else 0.25 - x0, x1 = ranges[xdim.name] - y0, y1 = ranges[ydim.name] + x0, x1 = ranges[xdim.name][range_type] + y0, y1 = ranges[ydim.name][range_type] xdiff = (x1-x0) ydiff = (y1-y0) if self.label_position == 'right': diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 3d51d2fcd1..70512e3afc 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -79,13 +79,10 @@ class BoxWhiskerPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): _stream_data = False # Plot does not support streaming data - def get_extents(self, element, ranges): - """ - Extents are set to '' and None because x-axis is categorical and - y-axis auto-ranges. - """ - yrange = ranges.get(element.vdims[0].name) - return ('', yrange[0], '', yrange[1]) + def get_extents(self, element, ranges, range_type='combined'): + return super(BoxWhiskerPlot, self).get_extents( + element, ranges, range_type, 'categorical', element.vdims[0] + ) def _get_axis_labels(self, *args, **kwargs): """ diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index ce1762ce9f..18689fee01 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -15,7 +15,8 @@ from ...core import OrderedDict, Dimension, Store from ...core.util import ( - match_spec, unique_iterator, basestring, max_range, isfinite, datetime_types + match_spec, unique_iterator, basestring, max_range, isfinite, + datetime_types, dt_to_int, dt64_to_dt ) from ...element import Raster, HeatMap from ...operation import interpolate_curve @@ -172,6 +173,7 @@ def update_handles(self, key, axis, element, ranges, style): return axis_kwargs + class AreaPlot(ChartPlot): show_legend = param.Boolean(default=False, doc=""" @@ -193,11 +195,21 @@ def init_artists(self, ax, plot_data, plot_kwargs): stack = fill_fn(*plot_data, **plot_kwargs) return {'artist': stack} - def get_extents(self, element, ranges): - vdims = element.vdims + def get_extents(self, element, ranges, range_type='combined'): + vdims = element.vdims[:2] vdim = vdims[0].name - ranges[vdim] = max_range([ranges[vd.name] for vd in vdims]) - return super(AreaPlot, self).get_extents(element, ranges) + if len(vdims) > 1: + new_range = {} + for r in ranges[vdim]: + new_range[r] = max_range([ranges[vd.name][r] for vd in vdims]) + ranges[vdim] = new_range + else: + s0, s1 = ranges[vdim]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s1 = max(s1, 0) if isfinite(s1) else 0 + ranges[vdim]['soft'] = (s0, s1) + return super(AreaPlot, self).get_extents(element, ranges, range_type) + @@ -244,8 +256,8 @@ def get_data(self, element, ranges, style): pos_error = element.dimension_values(pos_idx) return (xs, mean-neg_error, mean+pos_error), style, {} - def get_extents(self, element, ranges): - return ChartPlot.get_extents(self, element, ranges) + def get_extents(self, element, ranges, range_type='combined'): + return ChartPlot.get_extents(self, element, ranges, range_type) @@ -322,8 +334,9 @@ def _process_hist(self, hist): ylim = hist.range(1) isdatetime = False if edges.dtype.kind == 'M' or isinstance(edges[0], datetime_types): - edges = date2num([v.tolist() if isinstance(v, np.datetime64) else v for v in edges]) - xlim = date2num([v.tolist() if isinstance(v, np.datetime64) else v for v in xlim]) + edges = np.array([dt64_to_dt(e) if isinstance(e, np.datetime64) else e for e in edges]) + edges = date2num(edges) + xlim = tuple(dt_to_int(v, 'D') for v in xlim) isdatetime = True widths = np.diff(edges) return edges[:-1], hist_vals, widths, xlim+ylim, isdatetime @@ -349,12 +362,13 @@ def _compute_ticks(self, element, edges, widths, lims): return [xvals, labels] - def get_extents(self, element, ranges): - x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(element, ranges) - ylow, yhigh = element.get_dimension(1).range - y0 = ylow if isfinite(ylow) else np.nanmin([0, y0]) - y1 = yhigh if isfinite(yhigh) else np.nanmax([0, y1]) - return (x0, y0, x1, y1) + def get_extents(self, element, ranges, range_type='combined'): + ydim = element.get_dimension(1) + s0, s1 = ranges[ydim.name]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s0 = max(s1, 0) if isfinite(s1) else 0 + ranges[ydim.name]['soft'] = (s0, s1) + return super(HistogramPlot, self).get_extents(element, ranges, range_type) def _process_axsettings(self, hist, lims, ticks): @@ -662,7 +676,7 @@ def get_data(self, element, ranges, style): mag_dim = element.get_dimension(self.size_index) if mag_dim: magnitudes = element.dimension_values(mag_dim) - _, max_magnitude = ranges[mag_dim.name] + _, max_magnitude = ranges[mag_dim.name]['combined'] if self.normalize_lengths and max_magnitude != 0: magnitudes = magnitudes / max_magnitude else: @@ -789,13 +803,13 @@ def _compute_styles(self, element, style_groups): return style, color_groups, sopts - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): ngroups = len(self.values['group']) vdim = element.vdims[0].name if self.stack_index in range(element.ndims): return 0, 0, ngroups, np.NaN else: - vrange = ranges[vdim] + vrange = ranges[vdim]['combined'] return 0, np.nanmin([vrange[0], 0]), ngroups, vrange[1] @@ -954,17 +968,31 @@ def init_artists(self, ax, plot_args, plot_kwargs): ax.add_collection(line_segments) return {'artist': line_segments} - - def get_extents(self, element, ranges): - l, b, r, t = super(SpikesPlot, self).get_extents(element, ranges) - if len(element.dimensions()) == 1: - b, t = self.position, self.position+self.spike_length - else: - b = np.nanmin([0, b]) - t = np.nanmax([0, t]) + def get_extents(self, element, ranges, range_type='combined'): + if len(element.dimensions()) > 1: + ydim = element.get_dimension(1) + s0, s1 = ranges[ydim.name]['soft'] + s0 = min(s0, 0) if isfinite(s0) else 0 + s0 = max(s1, 0) if isfinite(s1) else 0 + ranges[ydim.name]['soft'] = (s0, s1) + l, b, r, t = super(SpikesPlot, self).get_extents(element, ranges, range_type) + if len(element.dimensions()) == 1 and range_type != 'hard': + if self.batched: + bs, ts = [], [] + # Iterate over current NdOverlay and compute extents + # from position and length plot options + frame = self.current_frame or self.hmap.last + for el in frame.values(): + opts = self.lookup_options(el, 'plot').options + pos = opts.get('position', self.position) + length = opts.get('spike_length', self.spike_length) + bs.append(pos) + ts.append(pos+length) + b, t = (np.nanmin(bs), np.nanmax(ts)) + else: + b, t = self.position, self.position+self.spike_length return l, b, r, t - def get_data(self, element, ranges, style): dimensions = element.dimensions(label=True) ndims = len(dimensions) @@ -989,7 +1017,7 @@ def get_data(self, element, ranges, style): if vs.dtype.kind == 'M' and i < len(dims): dt_format = Dimension.type_formatters[np.datetime64] dims[i] = dims[i](value_format=DateFormatter(dt_format)) - vs = date2num([v.tolist() if isinstance(v, np.datetime64) else v for v in vs]) + vs = np.array([dt_to_int(v, 'D') for v in vs]) cols.append(vs) clean_spikes.append(np.column_stack(cols)) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 424e453e61..a94da9494a 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -236,6 +236,19 @@ def _set_axis_formatter(self, axis, dim): axis.set_major_formatter(wrap_formatter(formatter)) + def get_aspect(self, xspan, yspan): + """ + Computes the aspect ratio of the plot + """ + if isinstance(self.aspect, (int, float)): + return self.aspect + elif self.aspect == 'square': + return 1 + elif self.aspect == 'equal': + return xspan/yspan + return 1 + + def _set_aspect(self, axes, aspect): """ Set the aspect on the axes based on the aspect setting. @@ -612,7 +625,10 @@ def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''): clim = (0, 0) categorical = False elif values.dtype.kind in 'uif': - clim = ranges[vdim.name] if vdim.name in ranges else element.range(vdim) + if vdim.name in ranges: + clim = ranges[vdim.name]['combined'] + else: + clim = element.range(vdim) if self.logz: # Lower clim must be >0 when logz=True # Choose the maximum between the lowest non-zero value @@ -749,7 +765,8 @@ class OverlayPlot(LegendPlot, GenericOverlayPlot): 'show_frame', 'show_grid', 'logx', 'logy', 'logz', 'xticks', 'yticks', 'zticks', 'xrotation', 'yrotation', 'zrotation', 'invert_xaxis', 'invert_yaxis', - 'invert_zaxis', 'title_format'] + 'invert_zaxis', 'title_format', 'padding', + 'xlim', 'ylim', 'zlim'] def __init__(self, overlay, ranges=None, **params): if 'projection' not in params: diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 2a168d9a85..cb3af611fe 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -97,14 +97,8 @@ def get_data(self, element, ranges, style): return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims} - def get_extents(self, element, ranges): - """ - Extents are set to '' and None because x-axis is categorical and - y-axis auto-ranges. - """ - x0, x1 = element.nodes.range(0) - y0, y1 = element.nodes.range(1) - return (x0, y0, x1, y1) + def get_extents(self, element, ranges, range_type='combined'): + return super(GraphPlot, self).get_extents(element.nodes, ranges, range_type) def init_artists(self, ax, plot_args, plot_kwargs): @@ -207,11 +201,13 @@ class ChordPlot(GraphPlot): _style_groups = ['edge', 'node', 'arc'] - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): """ A Chord plot is always drawn on a unit circle. """ xdim, ydim = element.nodes.kdims[:2] + if range_type not in ('combined', 'data'): + return xdim.range[0], ydim.range[0], xdim.range[1], ydim.range[1] rng = 1.1 if element.nodes.get_dimension(self.label_index) is None else 1.4 x0, x1 = max_range([xdim.range, (-rng, rng)]) y0, y1 = max_range([ydim.range, (-rng, rng)]) diff --git a/holoviews/plotting/mpl/heatmap.py b/holoviews/plotting/mpl/heatmap.py index d03023690d..d4a0ffdbd2 100644 --- a/holoviews/plotting/mpl/heatmap.py +++ b/holoviews/plotting/mpl/heatmap.py @@ -57,7 +57,7 @@ class HeatMapPlot(RasterPlot): ticks or function. If `None`, no ticks are shown.""") - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): ys, xs = element.gridded.interface.shape(element.gridded, gridded=True) return (0, 0, xs, ys) @@ -312,7 +312,9 @@ def _get_ticks(ticks, ticker): return ticks - def get_extents(self, view, ranges): + def get_extents(self, view, ranges, range_type='combined'): + if range_type == 'hard': + return (np.nan,)*4 return (0, 0, np.pi*2, self.max_radius+self.radius_outer) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 413c083dd7..d162a0aef3 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -40,9 +40,9 @@ def __init__(self, *args, **kwargs): self.invert_yaxis = not self.invert_yaxis - def get_extents(self, element, ranges): - extents = super(RasterPlot, self).get_extents(element, ranges) - if self.situate_axes: + def get_extents(self, element, ranges, range_type='combined'): + extents = super(RasterPlot, self).get_extents(element, ranges, range_type) + if self.situate_axes or range_type not in ('combined', 'data'): return extents else: if isinstance(element, Image): @@ -150,6 +150,9 @@ class RasterGridPlot(GridPlot, OverlayPlot): equivalent using subplots. """ + padding = param.Number(default=0.1, doc=""" + The amount of padding as a fraction of the total Grid size""") + # Parameters inherited from OverlayPlot that are not part of the # GridPlot interface. Some of these may be enabled in future in # conjunction with GridPlot. @@ -159,6 +162,7 @@ class RasterGridPlot(GridPlot, OverlayPlot): apply_ticks = param.Parameter(precedence=-1) batched = param.Parameter(precedence=-1) bgcolor = param.Parameter(precedence=-1) + default_span = param.Parameter(precedence=-1) invert_axes = param.Parameter(precedence=-1) invert_xaxis = param.Parameter(precedence=-1) invert_yaxis = param.Parameter(precedence=-1) @@ -172,6 +176,9 @@ class RasterGridPlot(GridPlot, OverlayPlot): logz = param.Parameter(precedence=-1) show_grid = param.Parameter(precedence=-1) style_grouping = param.Parameter(precedence=-1) + xlim = param.Parameter(precedence=-1) + ylim = param.Parameter(precedence=-1) + zlim = param.Parameter(precedence=-1) xticks = param.Parameter(precedence=-1) yticks = param.Parameter(precedence=-1) zticks = param.Parameter(precedence=-1) @@ -216,7 +223,9 @@ def __init__(self, layout, keys=None, dimensions=None, create_axes=False, ranges def _finalize_artist(self, key): pass - def get_extents(self, view, ranges): + def get_extents(self, view, ranges, range_type='combined'): + if range_type == 'hard': + return (np.nan,)*4 width, height, _, _, _, _ = self.border_extents return (0, 0, width, height) @@ -249,7 +258,7 @@ def initialize_plot(self, ranges=None): opts = self.lookup_options(pane, 'style')[self.cyclic_index] plot = self.handles['axis'].imshow(data, extent=(x,x+w, y, y+h), **opts) cdim = pane.vdims[0].name - valrange = match_spec(pane, ranges).get(cdim, pane.range(cdim)) + valrange = match_spec(pane, ranges).get(cdim, pane.range(cdim))['combined'] plot.set_clim(valrange) if data is None: plot.set_visible(False) diff --git a/holoviews/plotting/mpl/sankey.py b/holoviews/plotting/mpl/sankey.py index dcfb46293e..2958968c65 100644 --- a/holoviews/plotting/mpl/sankey.py +++ b/holoviews/plotting/mpl/sankey.py @@ -34,14 +34,16 @@ class SankeyPlot(GraphPlot): filled = True - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): """ A Chord plot is always drawn on a unit circle. """ + if range_type == 'extents': + return element.nodes.extents xdim, ydim = element.nodes.kdims[:2] xpad = .05 if self.label_index is None else 0.25 - x0, x1 = ranges[xdim.name] - y0, y1 = ranges[ydim.name] + x0, x1 = ranges[xdim.name][range_type] + y0, y1 = ranges[ydim.name][range_type] xdiff = (x1-x0) ydiff = (y1-y0) if self.label_position == 'right': diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py index e88e917491..25ba56c1c1 100644 --- a/holoviews/plotting/mpl/stats.py +++ b/holoviews/plotting/mpl/stats.py @@ -1,5 +1,4 @@ import param -import numpy as np from ...core.ndmapping import sorted_context from .chart import AreaPlot, ChartPlot @@ -44,7 +43,6 @@ class BivariatePlot(PolygonPlot): - class BoxPlot(ChartPlot): """ BoxPlot plots the ErrorBar Element type and supporting @@ -60,17 +58,19 @@ class BoxPlot(ChartPlot): _plot_methods = dict(single='boxplot') - def get_extents(self, element, ranges): - return (np.NaN,)*4 - + def get_extents(self, element, ranges, range_type='combined'): + return super(BoxPlot, self).get_extents( + element, ranges, range_type, 'categorical', element.vdims[0] + ) def get_data(self, element, ranges, style): - with sorted_context(False): - groups = element.groupby(element.kdims) + if element.kdims: + with sorted_context(False): + groups = element.groupby(element.kdims).data.items() + else: + groups = [(element.label, element)] data, labels = [], [] - - groups = groups.data.items() if element.kdims else [(element.label, element)] for key, group in groups: if element.kdims: label = ','.join([d.pprint_value(v) for d, v in zip(element.kdims, key)]) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 56580f6b1f..fcd3c5193b 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -23,7 +23,7 @@ from .util import (get_dynamic_mode, initialize_unbounded, dim_axis_label, attach_streams, traverse_setter, get_nested_streams, compute_overlayable_zorders, get_plot_frame, - split_dmap_overlay) + split_dmap_overlay, get_axis_padding, get_range) class Plot(param.Parameterized): @@ -178,17 +178,17 @@ class DimensionedPlot(Plot): """ fontsize = param.Parameter(default=None, allow_None=True, doc=""" - Specifies various fontsizes of the displayed text. + Specifies various font sizes of the displayed text. Finer control is available by supplying a dictionary where any - unmentioned keys reverts to the default sizes, e.g: + unmentioned keys revert to the default sizes, e.g: {'ticks':20, 'title':15, 'ylabel':5, 'xlabel':5, 'zlabel':5, 'legend':8, 'legend_title':13} - You can set the fontsize of 'zlabel', 'ylabel' and 'xlabel' together - using the 'labels' key.""") + You can set the font size of 'zlabel', 'ylabel' and 'xlabel' + together using the 'labels' key.""") #Allowed fontsize keys _fontsize_keys = ['xlabel','ylabel', 'zlabel', 'labels', 'ticks', @@ -211,7 +211,7 @@ class DimensionedPlot(Plot): Allows supplying a custom projection to transform the axis coordinates during display. Example projections include '3d' and 'polar' projections supported by some backends. Depending - on the backend custom projection objects may be supplied.""") + on the backend custom, projection objects may be supplied.""") def __init__(self, keys=None, dimensions=None, layout_dimensions=None, uniform=True, subplot=False, adjoined=None, layout_num=0, @@ -342,7 +342,7 @@ def _fontsize(self, key, label='fontsize', common=True): def compute_ranges(self, obj, key, ranges): """ - Given an object, a specific key and the normalization options + Given an object, a specific key, and the normalization options, this method will find the specified normalization options on the appropriate OptionTree, group the elements according to the selected normalization option (i.e. either per frame or @@ -387,7 +387,7 @@ def compute_ranges(self, obj, key, ranges): def _get_norm_opts(self, obj): """ Gets the normalization options for a LabelledData object by - traversing the object for to find elements and their ids. + traversing the object to find elements and their ids. The id is then used to select the appropriate OptionsTree, accumulating the normalization options into a dictionary. Returns a dictionary of normalization options for each @@ -438,12 +438,25 @@ def _compute_group_range(group, elements, ranges): group_ranges = OrderedDict() for el in elements: if isinstance(el, (Empty, Table)): continue - for dim in el.dimensions('ranges', label=True): - dim_range = el.range(dim) - if dim not in group_ranges: - group_ranges[dim] = [] - group_ranges[dim].append(dim_range) - ranges[group] = OrderedDict((k, util.max_range(v)) for k, v in group_ranges.items()) + for dim in el.dimensions('ranges'): + data_range = el.range(dim, dimension_range=False) + if dim.name not in group_ranges: + group_ranges[dim.name] = {'data': [], 'hard': [], 'soft': []} + group_ranges[dim.name]['data'].append(data_range) + group_ranges[dim.name]['hard'].append(dim.range) + group_ranges[dim.name]['soft'].append(dim.soft_range) + + dim_ranges = [] + for dim, values in group_ranges.items(): + hard_range = util.max_range(values['hard'], combined=False) + soft_range = util.max_range(values['soft']) + data_range = util.max_range(values['data']) + combined = util.dimension_range(data_range[0], data_range[1], + hard_range, soft_range) + dranges = {'data': data_range, 'hard': hard_range, + 'soft': soft_range, 'combined': combined} + dim_ranges.append((dim, dranges)) + ranges[group] = OrderedDict(dim_ranges) @classmethod @@ -457,7 +470,7 @@ def _traverse_options(cls, obj, opt_type, opts, specs=None, keyfn=None, defaults """ def lookup(x): """ - Looks up options for object, including plot defaults, + Looks up options for object, including plot defaults. keyfn determines returned key otherwise None key is used. """ options = cls.lookup_options(x, opt_type) @@ -584,6 +597,13 @@ class GenericElementPlot(DimensionedPlot): bgcolor = param.ClassSelector(class_=(str, tuple), default=None, doc=""" If set bgcolor overrides the background color of the axis.""") + default_span = param.ClassSelector(default=2.0, class_=(int, float, tuple), doc=""" + Defines the span of an axis if the axis range is zero, i.e. if + the lower and upper end of an axis are equal or no range is + defined at all. For example if there is a single datapoint at + 0 a default_span of 2.0 will result in axis ranges spanning + from -1 to 1.""") + invert_axes = param.Boolean(default=False, doc=""" Whether to invert the x- and y-axis""") @@ -599,6 +619,32 @@ class GenericElementPlot(DimensionedPlot): logy = param.Boolean(default=False, doc=""" Whether the y-axis of the plot will be a log axis.""") + padding = param.ClassSelector(default=0, class_=(int, float, tuple), doc=""" + Fraction by which to increase auto-ranged extents to make + datapoints more visible around borders. + + To compute padding, the axis whose screen size is largest is + chosen, and the range of that axis is increased by the + specified fraction along each axis. Other axes are then + padded ensuring that the amount of screen space devoted to + padding is equal for all axes. If specified as a tuple, the + int or float values in the tuple will be used for padding in + each axis, in order (x,y or x,y,z). + + For example, for padding=0.2 on a 800x800-pixel plot, an x-axis + with the range [0,10] will be padded by 20% to be [-1,11], while + a y-axis with a range [0,1000] will be padded to be [-100,1100], + which should make the padding be approximately the same number of + pixels. But if the same plot is changed to have a height of only + 200, the y-range will then be [-400,1400] so that the y-axis + padding will still match that of the x-axis. + + It is also possible to declare non-equal padding value for the + lower and upper bound of an axis by supplying nested tuples, + e.g. padding=(0.1, (0, 0.1)) will pad the x-axis lower and + upper bound as well as the y-axis upper bound by a fraction of + 0.1 while the y-axis lower bound is not padded at all.""") + show_legend = param.Boolean(default=True, doc=""" Whether to show legend for the plot.""") @@ -608,16 +654,28 @@ class GenericElementPlot(DimensionedPlot): xaxis = param.ObjectSelector(default='bottom', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None, True, False], doc=""" - Whether and where to display the xaxis, bare options allow suppressing - all axis labels including ticks and xlabel. Valid options are 'top', - 'bottom', 'bare', 'top-bare' and 'bottom-bare'.""") + Whether and where to display the xaxis. + The "bare" options allow suppressing all axis labels, including ticks and xlabel. + Valid options are 'top', 'bottom', 'bare', 'top-bare' and 'bottom-bare'.""") yaxis = param.ObjectSelector(default='left', objects=['left', 'right', 'bare', 'left-bare', 'right-bare', None, True, False], doc=""" - Whether and where to display the yaxis, bare options allow suppressing - all axis labels including ticks and ylabel. Valid options are 'left', - 'right', 'bare' 'left-bare' and 'right-bare'.""") + Whether and where to display the yaxis. + The "bare" options allow suppressing all axis labels, including ticks and ylabel. + Valid options are 'left', 'right', 'bare', 'left-bare' and 'right-bare'.""") + + xlim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc=""" + User-specified x-axis range limits for the plot, as a tuple (low,high). + If specified, takes precedence over data and dimension ranges.""") + + ylim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc=""" + User-specified x-axis range limits for the plot, as a tuple (low,high). + If specified, takes precedence over data and dimension ranges.""") + + zlim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc=""" + User-specified z-axis range limits for the plot, as a tuple (low,high). + If specified, takes precedence over data and dimension ranges.""") xrotation = param.Integer(default=None, bounds=(0, 360), doc=""" Rotation angle of the xticks.""") @@ -627,12 +685,12 @@ class GenericElementPlot(DimensionedPlot): xticks = param.Parameter(default=None, doc=""" Ticks along x-axis specified as an integer, explicit list of - tick locations or bokeh Ticker object. If set to None default + tick locations, or bokeh Ticker object. If set to None default bokeh ticking behavior is applied.""") yticks = param.Parameter(default=None, doc=""" Ticks along y-axis specified as an integer, explicit list of - tick locations or bokeh Ticker object. If set to None + tick locations, or bokeh Ticker object. If set to None default bokeh ticking behavior is applied.""") # A dictionary mapping of the plot methods used to draw the @@ -754,50 +812,129 @@ def _execute_hooks(self, element): self.warning("Plotting hook %r could not be applied:\n\n %s" % (hook, e)) - def get_extents(self, view, ranges): + def get_aspect(self, xspan, yspan): """ - Gets the extents for the axes from the current View. The globally - computed ranges can optionally override the extents. + Should define the aspect ratio of the plot. """ - ndims = len(view.dimensions()) - num = 6 if self.projection == '3d' else 4 - if self.apply_ranges: - if ranges: - dims = view.dimensions() - x0, x1 = ranges[dims[0].name] - if ndims > 1: - y0, y1 = ranges[dims[1].name] - else: - y0, y1 = (np.NaN, np.NaN) - if self.projection == '3d': - if len(dims) > 2: - z0, z1 = ranges[dims[2].name] - else: - z0, z1 = np.NaN, np.NaN - else: - x0, x1 = view.range(0) - y0, y1 = view.range(1) if ndims > 1 else (np.NaN, np.NaN) - if self.projection == '3d': - z0, z1 = view.range(2) - if self.projection == '3d': - range_extents = (x0, y0, z0, x1, y1, z1) + + + def get_padding(self, extents): + """ + Computes padding along the axes taking into account the plot aspect. + """ + (x0, y0, z0, x1, y1, z1) = extents + padding = 0 if self.overlaid else self.padding + xpad, ypad, zpad = get_axis_padding(padding) + if not self.overlaid and not self.batched: + xspan = x1-x0 if util.is_number(x0) and util.is_number(x1) else None + yspan = y1-y0 if util.is_number(y0) and util.is_number(y1) else None + aspect = self.get_aspect(xspan, yspan) + if aspect > 1: + xpad = tuple(xp/aspect for xp in xpad) if isinstance(xpad, tuple) else xpad/aspect else: - range_extents = (x0, y0, x1, y1) + ypad = tuple(yp*aspect for yp in ypad) if isinstance(ypad, tuple) else ypad*aspect + return xpad, ypad, zpad + + + def _get_range_extents(self, element, ranges, range_type, xdim, ydim, zdim): + dims = element.dimensions() + ndims = len(dims) + xdim = xdim or (dims[0] if ndims else None) + ydim = ydim or (dims[1] if ndims > 1 else None) + if self.projection == '3d': + zdim = zdim or (dims[2] if ndims > 2 else None) else: - range_extents = (np.NaN,) * num + zdim = None + + (x0, x1), xsrange, xhrange = get_range(element, ranges, xdim) + (y0, y1), ysrange, yhrange = get_range(element, ranges, ydim) + (z0, z1), zsrange, zhrange = get_range(element, ranges, zdim) + + if not self.overlaid and not self.batched: + xspan, yspan, zspan = (v/2. for v in get_axis_padding(self.default_span)) + if util.is_number(x0) and x0 == x1: x0, x1 = x0-xspan, x1+xspan + if util.is_number(x0) and y0 == y1: y0, y1 = y0-yspan, y1+yspan + if util.is_number(z0) and z0 == z1: z0, z1 = z0-zspan, z1+zspan + xpad, ypad, zpad = self.get_padding((x0, y0, z0, x1, y1, z1)) + + if range_type == 'soft': + x0, x1 = xsrange + elif range_type == 'hard': + x0, x1 = xhrange + elif xdim == 'categorical': + x0, x1 = '', '' + elif range_type == 'combined': + x0, x1 = util.dimension_range(x0, x1, xhrange, xsrange, xpad, self.logx) + + if range_type == 'soft': + y0, y1 = ysrange + elif range_type == 'hard': + y0, y1 = yhrange + elif range_type == 'combined': + y0, y1 = util.dimension_range(y0, y1, yhrange, ysrange, ypad, self.logy) + elif ydim == 'categorical': + y0, y1 = '', '' + elif ydim is None: + y0, y1 = np.NaN, np.NaN + + if self.projection == '3d': + if range_type == 'soft': + z0, z1 = zsrange + elif range_type == 'data': + z0, z1 = zhrange + elif range_type=='combined': + z0, z1 = util.dimension_range(z0, z1, zhrange, zsrange, zpad, self.logz) + elif zdim == 'categorical': + z0, z1 = '', '' + elif zdim is None: + z0, z1 = np.NaN, np.NaN + return (x0, y0, z0, x1, y1, z1) + return (x0, y0, x1, y1) + + + def get_extents(self, element, ranges, range_type='combined', xdim=None, ydim=None, zdim=None): + """ + Gets the extents for the axes from the current Element. The globally + computed ranges can optionally override the extents. + + The extents are computed by combining the data ranges, extents + and dimension ranges. Each of these can be obtained individually + by setting the range_type to one of: + + * 'data': Just the data ranges + * 'extents': Element.extents + * 'soft': Dimension.soft_range values + * 'hard': Dimension.range values + + To obtain the combined range, which includes range padding the + default may be used: - if self.apply_extents: - norm_opts = self.lookup_options(view, 'norm').options + * 'combined': All the range types combined and padding applied + + This allows Overlay plots to obtain each range and combine them + appropriately for all the objects in the overlay. + """ + num = 6 if self.projection == '3d' else 4 + if self.apply_extents and range_type in ('combined', 'extents'): + norm_opts = self.lookup_options(element, 'norm').options if norm_opts.get('framewise', False) or self.dynamic: - extents = view.extents + extents = element.extents else: extent_list = self.hmap.traverse(lambda x: x.extents, [Element]) extents = util.max_extents(extent_list, self.projection == '3d') else: extents = (np.NaN,) * num + if range_type == 'extents': + return extents + + if self.apply_ranges: + range_extents = self._get_range_extents(element, ranges, range_type, xdim, ydim, zdim) + else: + range_extents = (np.NaN,) * num + if getattr(self, 'shared_axes', False) and self.subplot: - return util.max_extents([range_extents, extents], self.projection == '3d') + combined = util.max_extents([range_extents, extents], self.projection == '3d') else: max_extent = [] for l1, l2 in zip(range_extents, extents): @@ -805,7 +942,19 @@ def get_extents(self, view, ranges): max_extent.append(l2) else: max_extent.append(l1) - return tuple(max_extent) + combined = tuple(max_extent) + + if self.projection == '3d': + x0, y0, z0, x1, y1, z1 = combined + else: + x0, y0, x1, y1 = combined + + x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) + y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) + if self.projection == '3d': + z0, z1 = util.dimension_range(z0, z1, self.zlim, (None, None)) + return (x0, y0, z0, x1, y1, z1) + return (x0, y0, x1, y1) def _get_axis_labels(self, dimensions, xlabel=None, ylabel=None, zlabel=None): @@ -1073,14 +1222,18 @@ def _update_subplot(self, subplot, spec): subplot.overlay_dims = util.OrderedDict(new_dims) - def get_extents(self, overlay, ranges): - extents = [] + def _get_subplot_extents(self, overlay, ranges, range_type): + """ + Iterates over all subplots and collects the extents of each. + """ + extents = defaultdict(list) items = overlay.items() if self.batched and self.subplots: subplot = list(self.subplots.values())[0] subplots = [(k, subplot) for k in overlay.data.keys()] else: subplots = self.subplots.items() + for key, subplot in subplots: found = False if subplot is None: @@ -1093,14 +1246,67 @@ def get_extents(self, overlay, ranges): break if not found: layer = None - if layer is not None and subplot.apply_ranges: - if isinstance(layer, CompositeOverlay): - sp_ranges = ranges - else: - sp_ranges = util.match_spec(layer, ranges) if ranges else {} - extents.append(subplot.get_extents(layer, sp_ranges)) - return util.max_extents(extents, self.projection == '3d') + if layer is None or not subplot.apply_ranges: + continue + if isinstance(layer, CompositeOverlay): + sp_ranges = ranges + else: + sp_ranges = util.match_spec(layer, ranges) if ranges else {} + range_types = ('extents', 'soft', 'hard', 'data') if range_type == 'combined' else (range_type,) + for rt in range_types: + extents[rt].append(subplot.get_extents(layer, sp_ranges, range_type=rt)) + return extents + + + def get_extents(self, overlay, ranges, range_type='combined'): + subplot_extents = self._get_subplot_extents(overlay, ranges, range_type) + zrange = self.projection == '3d' + extents = {k: util.max_extents(rs, zrange) for k, rs in subplot_extents.items()} + if range_type != 'combined': + return extents[range_type] + + # Unpack extents + if len(extents['data']) == 6: + x0, y0, z0, x1, y1, z1 = extents['data'] + sx0, sy0, sz0, sx1, sy1, sz1 = extents['soft'] + hx0, hy0, hz0, hx1, hy1, hz1 = extents['hard'] + else: + x0, y0, x1, y1 = extents['data'] + sx0, sy0, sx1, sy1 = extents['soft'] + hx0, hy0, hx1, hy1 = extents['hard'] + z0, z1 = np.NaN, np.NaN + + # Apply minimum span + xspan, yspan, zspan = (v/2. for v in get_axis_padding(self.default_span)) + if util.is_number(x0) and x0 == x1: x0, x1 = x0-xspan, x1+xspan + if util.is_number(x0) and y0 == y1: y0, y1 = y0-yspan, y1+yspan + if util.is_number(z0) and z0 == z1: z0, z1 = z0-zspan, z1+zspan + + # Apply padding + xpad, ypad, zpad = self.get_padding((x0, y0, z0, x1, y1, z1)) + x0, x1 = util.dimension_range(x0, x1, (hx0, hx1), (sx0, sx1), xpad, self.logx) + y0, y1 = util.dimension_range(y0, y1, (hy0, hy1), (sy0, sy1), ypad, self.logy) + if len(extents['data']) == 6: + z0, z1 = util.dimension_range(z0, z1, (hz0, hz1), (sz0, sz1), zpad, self.logz) + padded = (x0, y0, z0, x1, y1, z1) + else: + padded = (x0, y0, x1, y1) + + # Combine with Element.extents + combined = util.max_extents([padded, extents['extents']], zrange) + if self.projection == '3d': + x0, y0, z0, x1, y1, z1 = combined + else: + x0, y0, x1, y1 = combined + + # Apply xlim, ylim, zlim plot option + x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) + y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) + if self.projection == '3d': + z0, z1 = util.dimension_range(z0, z1, getattr(self, 'zlim', (None, None)), (None, None)) + return (x0, y0, z0, x1, y1, z1) + return (x0, y0, x1, y1) class GenericCompositePlot(DimensionedPlot): diff --git a/holoviews/plotting/plotly/chart.py b/holoviews/plotting/plotly/chart.py index 7b8803555a..9a1546b996 100644 --- a/holoviews/plotting/plotly/chart.py +++ b/holoviews/plotting/plotly/chart.py @@ -221,5 +221,5 @@ def generate_plot(self, key, ranges): self.handles['fig'] = fig return fig - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): return (None, None, None, None) diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index 39e544c8c8..e6ec77ba10 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -149,6 +149,13 @@ def get_data(self, element, ranges): return {} + def get_aspect(self, xspan, yspan): + """ + Computes the aspect ratio of the plot + """ + return self.width/self.height + + def init_layout(self, key, element, ranges, xdim=None, ydim=None): l, b, r, t = self.get_extents(element, ranges) @@ -180,7 +187,7 @@ def init_layout(self, key, element, ranges, xdim=None, ydim=None): options['yaxis'] = yaxis l, b, r, t = self.margins - margin = go.Margin(l=l, r=r,b=b, t=t, pad=4) + margin = go.Margin(l=l, r=r, b=b, t=t, pad=4) return go.Layout(width=self.width, height=self.height, title=self._format_title(key, separator=' '), plot_bgcolor=self.bgcolor, margin=margin, @@ -226,7 +233,10 @@ def get_color_opts(self, dim, element, ranges, style): opts['reversescale'] = True opts['colorscale'] = cmap if dim: - cmin, cmax = ranges.get(dim.name, element.range(dim.name)) + if dim.name in ranges: + cmin, cmax = ranges[dim.name]['combined'] + else: + cmin, cmax = element.range(dim.name) opts['cmin'] = cmin opts['cmax'] = cmax opts['cauto'] = False diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index ec048b6c80..2377520df2 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -36,7 +36,7 @@ def get_data(self, element, ranges): class HeatMapPlot(RasterPlot): - def get_extents(self, element, ranges): + def get_extents(self, element, ranges, range_type='combined'): return (np.NaN,)*4 def get_data(self, element, ranges): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 646e52473d..0a14e52566 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -309,6 +309,45 @@ def compute_sizes(sizes, size_fn, scaling_factor, scaling_method, base_size): return (base_size*scaling_factor*sizes) +def get_axis_padding(padding): + """ + Process a padding value supplied as a tuple or number and returns + padding values for x-, y- and z-axis. + """ + if isinstance(padding, tuple): + if len(padding) == 2: + xpad, ypad = padding + zpad = 0 + elif len(padding) == 3: + xpad, ypad, zpad = padding + else: + raise ValueError('Padding must be supplied as an number applied ' + 'to all axes or a length two or three tuple ' + 'corresponding to the x-, y- and optionally z-axis') + else: + xpad, ypad, zpad = (padding,)*3 + return (xpad, ypad, zpad) + + +def get_range(element, ranges, dimension): + """ + Computes the data, soft- and hard-range along a dimension given + an element and a dictionary of ranges. + """ + if dimension and dimension != 'categorical': + if ranges and dimension.name in ranges: + drange = ranges[dimension.name]['data'] + srange = ranges[dimension.name]['soft'] + hrange = ranges[dimension.name]['hard'] + else: + drange = element.range(dimension, dimension_range=False) + srange = dimension.soft_range + hrange = dimension.range + else: + drange = srange = hrange = (np.NaN, np.NaN) + return drange, srange, hrange + + def get_sideplot_ranges(plot, element, main, ranges): """ Utility to find the range for an adjoined @@ -329,7 +368,7 @@ def get_sideplot_ranges(plot, element, main, ranges): ranges = match_spec(range_item.last, ranges) if dim.name in ranges: - main_range = ranges[dim.name] + main_range = ranges[dim.name]['combined'] else: framewise = plot.lookup_options(range_item.last, 'norm').options.get('framewise') if framewise and range_item.get(key, False): diff --git a/tests/element/testannotations.py b/tests/element/testannotations.py index a5242f94d3..530d39bb76 100644 --- a/tests/element/testannotations.py +++ b/tests/element/testannotations.py @@ -29,13 +29,13 @@ def test_text_string_position(self): def test_hline_dimension_values(self): hline = HLine(0) - self.assertEqual(hline.range(0), (np.NaN, np.NaN)) + self.assertTrue(all(not np.isfinite(v) for v in hline.range(0))) self.assertEqual(hline.range(1), (0, 0)) def test_vline_dimension_values(self): hline = VLine(0) self.assertEqual(hline.range(0), (0, 0)) - self.assertEqual(hline.range(1), (np.NaN, np.NaN)) + self.assertTrue(all(not np.isfinite(v) for v in hline.range(1))) def test_arrow_redim_range_aux(self): annotations = Arrow(0, 0) diff --git a/tests/plotting/bokeh/testareaplot.py b/tests/plotting/bokeh/testareaplot.py index 911471a79d..2f6b8d786c 100644 --- a/tests/plotting/bokeh/testareaplot.py +++ b/tests/plotting/bokeh/testareaplot.py @@ -22,3 +22,93 @@ def test_area_empty(self): cds = plot.handles['cds'] self.assertEqual(cds.data['x'], []) self.assertEqual(cds.data['y'], []) + + def test_area_padding_square(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_area_padding_square_per_axis(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 1.0) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.4) + + def test_area_with_lower_vdim(self): + area = Area([(1, 0.5, 1), (2, 1.5, 2), (3, 2.5, 3)], vdims=['y', 'y2']).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.25) + self.assertEqual(y_range.end, 3.25) + + def test_area_padding_negative(self): + area = Area([(1, -1), (2, -2), (3, -3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, -3.2) + self.assertEqual(y_range.end, 0) + + def test_area_padding_mixed(self): + area = Area([(1, 1), (2, -2), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, -2.5) + self.assertEqual(y_range.end, 3.5) + + def test_area_padding_hard_range(self): + area = Area([(1, 1), (2, 2), (3, 3)]).redim.range(y=(0, 4)).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 4) + + def test_area_padding_soft_range(self): + area = Area([(1, 1), (2, 2), (3, 3)]).redim.soft_range(y=(0, 3.5)).options(padding=0.1) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.5) + + def test_area_padding_nonsquare(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.9) + self.assertEqual(x_range.end, 3.1) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_area_padding_logx(self): + area = Area([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_area_padding_logy(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(area) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.033483695221017122) + self.assertEqual(y_range.end, 3.3483695221017129) diff --git a/tests/plotting/bokeh/testbarplot.py b/tests/plotting/bokeh/testbarplot.py index b205a97ca5..d1a9d60d88 100644 --- a/tests/plotting/bokeh/testbarplot.py +++ b/tests/plotting/bokeh/testbarplot.py @@ -78,4 +78,47 @@ def test_bars_logy_explicit_range(self): self.assertEqual(source.data['Value'], np.array([1, 2, 3])) self.assertEqual(glyph.bottom, 0.001) self.assertEqual(y_range.start, 0.001) - self.assertEqual(y_range.end, 3.) + self.assertEqual(y_range.end, 3.0000000000000013) + + def test_bars_padding_square(self): + points = Bars([(1, 2), (2, -1), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, -1.4) + self.assertEqual(y_range.end, 3.4) + + def test_bars_padding_square_positive(self): + points = Bars([(1, 2), (2, 1), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_bars_padding_square_negative(self): + points = Bars([(1, -2), (2, -1), (3, -3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, -3.2) + self.assertEqual(y_range.end, 0) + + def test_bars_padding_nonsquare(self): + bars = Bars([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(bars) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_bars_padding_logx(self): + bars = Bars([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(bars) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_bars_padding_logy(self): + bars = Bars([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(bars) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, 0.033483695221017122) + self.assertEqual(y_range.end, 3.3483695221017129) + diff --git a/tests/plotting/bokeh/testboxwhiskerplot.py b/tests/plotting/bokeh/testboxwhiskerplot.py index 50948af666..c880d574d5 100644 --- a/tests/plotting/bokeh/testboxwhiskerplot.py +++ b/tests/plotting/bokeh/testboxwhiskerplot.py @@ -35,3 +35,10 @@ def test_box_whisker_hover(self): self.assertIn(plot.handles['vbar_1_glyph_renderer'], hover_tool.renderers) self.assertIn(plot.handles['vbar_2_glyph_renderer'], hover_tool.renderers) self.assertIn(plot.handles['circle_1_glyph_renderer'], hover_tool.renderers) + + def test_box_whisker_padding_square(self): + curve = BoxWhisker([1, 2, 3]).options(padding=0.1) + plot = bokeh_renderer.get_plot(curve) + y_range = plot.handles['y_range'] + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) diff --git a/tests/plotting/bokeh/testcurveplot.py b/tests/plotting/bokeh/testcurveplot.py index 0a1aed0efb..a50cc94457 100644 --- a/tests/plotting/bokeh/testcurveplot.py +++ b/tests/plotting/bokeh/testcurveplot.py @@ -243,3 +243,97 @@ def test_curve_xticks_list_of_tuples_yaxis(self): plot = bokeh_renderer.get_plot(curve).state self.assertIsInstance(plot.yaxis[0].ticker, FixedTicker) self.assertEqual(plot.yaxis[0].major_label_overrides, dict(ticks)) + + def test_curve_padding_square(self): + curve = Curve([1, 2, 3]).options(padding=0.1) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_square_per_axis(self): + curve = Curve([1, 2, 3]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.4) + + def test_curve_padding_hard_xrange(self): + curve = Curve([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 3) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_soft_xrange(self): + curve = Curve([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 3) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_unequal(self): + curve = Curve([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.1) + self.assertEqual(x_range.end, 2.1) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_nonsquare(self): + curve = Curve([1, 2, 3]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.1) + self.assertEqual(x_range.end, 2.1) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_logx(self): + curve = Curve([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_logy(self): + curve = Curve([1, 2, 3]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.89595845984076228) + self.assertEqual(y_range.end, 3.3483695221017129) + + def test_curve_padding_datetime_square(self): + curve = Curve([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T19:12:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T04:48:00.000000000')) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_datetime_nonsquare(self): + curve = Curve([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1, width=600 + ) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T21:36:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T02:24:00.000000000')) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) diff --git a/tests/plotting/bokeh/testerrorbarplot.py b/tests/plotting/bokeh/testerrorbarplot.py new file mode 100644 index 0000000000..9c72991fd6 --- /dev/null +++ b/tests/plotting/bokeh/testerrorbarplot.py @@ -0,0 +1,60 @@ +from holoviews.element import ErrorBars + +from .testplot import TestBokehPlot, bokeh_renderer + + +class TestErrorBarsPlot(TestBokehPlot): + + def test_errorbars_padding_square(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_errorbars_padding_hard_range(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).redim.range(y=(0, 4)).options(padding=0.1) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 4) + + def test_errorbars_padding_soft_range(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).redim.soft_range(y=(0, 3.5)).options(padding=0.1) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.8) + + def test_errorbars_padding_nonsquare(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.9) + self.assertEqual(x_range.end, 3.1) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_errorbars_padding_logx(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3,3, 0.5)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_errorbars_padding_logy(self): + errorbars = ErrorBars([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(errorbars) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.41158562699652224) + self.assertEqual(y_range.end, 4.2518491541367327) diff --git a/tests/plotting/bokeh/testhistogramplot.py b/tests/plotting/bokeh/testhistogramplot.py index 5bda5f3928..4ef525d4b2 100644 --- a/tests/plotting/bokeh/testhistogramplot.py +++ b/tests/plotting/bokeh/testhistogramplot.py @@ -2,7 +2,7 @@ import numpy as np -from holoviews.element import Image, Points, Dataset +from holoviews.element import Image, Points, Dataset, Histogram from holoviews.operation import histogram from bokeh.models import DatetimeAxis @@ -72,3 +72,79 @@ def test_histogram_datetime64_plot(self): self.assertIsInstance(xaxis, DatetimeAxis) self.assertEqual(range_x.start, np.datetime64('2017-01-01T00:00:00.000000', 'us')) self.assertEqual(range_x.end, np.datetime64('2017-01-04T00:00:00.000000', 'us')) + + def test_histogram_padding_square(self): + points = Histogram([(1, 2), (2, -1), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.19999999999999996) + self.assertEqual(x_range.end, 3.8) + self.assertEqual(y_range.start, -1.4) + self.assertEqual(y_range.end, 3.4) + + def test_histogram_padding_square_positive(self): + points = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.19999999999999996) + self.assertEqual(x_range.end, 3.8) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_histogram_padding_square_negative(self): + points = Histogram([(1, -2), (2, -1), (3, -3)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.19999999999999996) + self.assertEqual(x_range.end, 3.8) + self.assertEqual(y_range.start, -3.2) + self.assertEqual(y_range.end, 0) + + def test_histogram_padding_nonsquare(self): + histogram = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(histogram) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.35) + self.assertEqual(x_range.end, 3.65) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_histogram_padding_logx(self): + histogram = Histogram([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(histogram) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.41158562699652224) + self.assertEqual(x_range.end, 4.2518491541367327) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_histogram_padding_logy(self): + histogram = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(histogram) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.19999999999999996) + self.assertEqual(x_range.end, 3.8) + self.assertEqual(y_range.start, 0.033483695221017122) + self.assertEqual(y_range.end, 3.3483695221017129) + + def test_histogram_padding_datetime_square(self): + histogram = Histogram([(np.datetime64('2016-04-0%d' % i, 'ns'), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = bokeh_renderer.get_plot(histogram) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T04:48:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T19:12:00.000000000')) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_histogram_padding_datetime_nonsquare(self): + histogram = Histogram([(np.datetime64('2016-04-0%d' % i, 'ns'), i) for i in range(1, 4)]).options( + padding=0.1, width=600 + ) + plot = bokeh_renderer.get_plot(histogram) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T08:24:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T15:36:00.000000000')) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) diff --git a/tests/plotting/bokeh/testpointplot.py b/tests/plotting/bokeh/testpointplot.py index 637ecad960..90cf47a6f3 100644 --- a/tests/plotting/bokeh/testpointplot.py +++ b/tests/plotting/bokeh/testpointplot.py @@ -216,3 +216,97 @@ def test_points_overlay_categorical_xaxis_invert_axes(self): y_range = plot.handles['y_range'] self.assertIsInstance(y_range, FactorRange) self.assertEqual(y_range.factors, ['A', 'B', 'C', 'D']) + + def test_points_padding_square(self): + points = Points([1, 2, 3]).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_curve_padding_square_per_axis(self): + curve = Points([1, 2, 3]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = bokeh_renderer.get_plot(curve) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.4) + + def test_points_padding_unequal(self): + points = Points([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.1) + self.assertEqual(x_range.end, 2.1) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_nonsquare(self): + points = Points([1, 2, 3]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.1) + self.assertEqual(x_range.end, 2.1) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_logx(self): + points = Points([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_logy(self): + points = Points([1, 2, 3]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 2.2) + self.assertEqual(y_range.start, 0.89595845984076228) + self.assertEqual(y_range.end, 3.3483695221017129) + + def test_points_padding_datetime_square(self): + points = Points([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T19:12:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T04:48:00.000000000')) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_datetime_nonsquare(self): + points = Points([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1, width=600 + ) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T21:36:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T02:24:00.000000000')) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_hard_xrange(self): + points = Points([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 3) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) + + def test_points_padding_soft_xrange(self): + points = Points([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(points) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, -0.2) + self.assertEqual(x_range.end, 3) + self.assertEqual(y_range.start, 0.8) + self.assertEqual(y_range.end, 3.2) diff --git a/tests/plotting/bokeh/testspikesplot.py b/tests/plotting/bokeh/testspikesplot.py index 09922a3231..fbe857ab9e 100644 --- a/tests/plotting/bokeh/testspikesplot.py +++ b/tests/plotting/bokeh/testspikesplot.py @@ -28,3 +28,83 @@ def test_batched_spike_plot(self): plot = bokeh_renderer.get_plot(overlay) extents = plot.get_extents(overlay, {}) self.assertEqual(extents, (0, 0, 9, 1)) + + def test_spikes_padding_square(self): + spikes = Spikes([1, 2, 3]).options(padding=0.1) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + + def test_spikes_padding_square_heights(self): + spikes = Spikes([(1, 1), (2, 2), (3, 3)], vdims=['Height']).options(padding=0.1) + plot = bokeh_renderer.get_plot(spikes) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_spikes_padding_hard_xrange(self): + spikes = Spikes([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 3) + + def test_spikes_padding_soft_xrange(self): + spikes = Spikes([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0) + self.assertEqual(x_range.end, 3.2) + + def test_spikes_padding_unequal(self): + spikes = Spikes([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0.9) + self.assertEqual(x_range.end, 3.1) + + def test_spikes_padding_nonsquare(self): + spikes = Spikes([1, 2, 3]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0.9) + self.assertEqual(x_range.end, 3.1) + + def test_spikes_padding_logx(self): + spikes = Spikes([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + + def test_spikes_padding_datetime_square(self): + spikes = Spikes([np.datetime64('2016-04-0%d' % i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T19:12:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T04:48:00.000000000')) + + def test_spikes_padding_datetime_square_heights(self): + spikes = Spikes([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)], vdims=['Height']).options( + padding=0.1 + ) + plot = bokeh_renderer.get_plot(spikes) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T19:12:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T04:48:00.000000000')) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.2) + + def test_spikes_padding_datetime_nonsquare(self): + spikes = Spikes([np.datetime64('2016-04-0%d' % i) for i in range(1, 4)]).options( + padding=0.1, width=600 + ) + plot = bokeh_renderer.get_plot(spikes) + x_range = plot.handles['x_range'] + self.assertEqual(x_range.start, np.datetime64('2016-03-31T21:36:00.000000000')) + self.assertEqual(x_range.end, np.datetime64('2016-04-03T02:24:00.000000000')) diff --git a/tests/plotting/bokeh/testspreadplot.py b/tests/plotting/bokeh/testspreadplot.py index 146ee7266b..299ab60b6d 100644 --- a/tests/plotting/bokeh/testspreadplot.py +++ b/tests/plotting/bokeh/testspreadplot.py @@ -7,7 +7,6 @@ from .testplot import TestBokehPlot, bokeh_renderer - class TestSpreadPlot(TestBokehPlot): def test_spread_stream_data(self): @@ -35,3 +34,57 @@ def test_spread_empty(self): cds = plot.handles['cds'] self.assertEqual(cds.data['x'], []) self.assertEqual(cds.data['y'], []) + + def test_spread_padding_square(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_spread_padding_hard_range(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).redim.range(y=(0, 4)).options(padding=0.1) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 4) + + def test_spread_padding_soft_range(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).redim.soft_range(y=(0, 3.5)).options(padding=0.1) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0) + self.assertEqual(y_range.end, 3.8) + + def test_spread_padding_nonsquare(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1, width=600) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.9) + self.assertEqual(x_range.end, 3.1) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_spread_padding_logx(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3,3, 0.5)]).options(padding=0.1, logx=True) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.89595845984076228) + self.assertEqual(x_range.end, 3.3483695221017129) + self.assertEqual(y_range.start, 0.19999999999999996) + self.assertEqual(y_range.end, 3.8) + + def test_spread_padding_logy(self): + spread = Spread([(1, 1, 0.5), (2, 2, 0.5), (3, 3, 0.5)]).options(padding=0.1, logy=True) + plot = bokeh_renderer.get_plot(spread) + x_range, y_range = plot.handles['x_range'], plot.handles['y_range'] + self.assertEqual(x_range.start, 0.8) + self.assertEqual(x_range.end, 3.2) + self.assertEqual(y_range.start, 0.41158562699652224) + self.assertEqual(y_range.end, 4.2518491541367327) diff --git a/tests/plotting/matplotlib/testareaplot.py b/tests/plotting/matplotlib/testareaplot.py new file mode 100644 index 0000000000..687d4855ab --- /dev/null +++ b/tests/plotting/matplotlib/testareaplot.py @@ -0,0 +1,96 @@ +from holoviews.element import Area + +from .testplot import TestMPLPlot, mpl_renderer + + +class TestAreaPlot(TestMPLPlot): + + def test_area_padding_square(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_area_padding_square_per_axis(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 1) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.4) + + def test_area_with_lower_vdim(self): + area = Area([(1, 0.5, 1), (2, 1.5, 2), (3, 2.5, 3)], vdims=['y', 'y2']).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0.25) + self.assertEqual(y_range[1], 3.25) + + def test_area_padding_negative(self): + area = Area([(1, -1), (2, -2), (3, -3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], -3.2) + self.assertEqual(y_range[1], 0) + + def test_area_padding_mixed(self): + area = Area([(1, 1), (2, -2), (3, 3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], -2.5) + self.assertEqual(y_range[1], 3.5) + + def test_area_padding_hard_range(self): + area = Area([(1, 1), (2, 2), (3, 3)]).redim.range(y=(0, 4)).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 4) + + def test_area_padding_soft_range(self): + area = Area([(1, 1), (2, 2), (3, 3)]).redim.soft_range(y=(0, 3.5)).options(padding=0.1) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.5) + + def test_area_padding_nonsquare(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.9) + self.assertEqual(x_range[1], 3.1) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_area_padding_logx(self): + area = Area([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.89595845984076228) + self.assertEqual(x_range[1], 3.3483695221017129) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_area_padding_logy(self): + area = Area([(1, 1), (2, 2), (3, 3)]).options(padding=0.1, logy=True) + plot = mpl_renderer.get_plot(area) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 1) + self.assertEqual(y_range[1], 3.3483695221017129) diff --git a/tests/plotting/matplotlib/testboxwhisker.py b/tests/plotting/matplotlib/testboxwhisker.py index fe9ca0c5f1..9e34cd93a7 100644 --- a/tests/plotting/matplotlib/testboxwhisker.py +++ b/tests/plotting/matplotlib/testboxwhisker.py @@ -24,3 +24,10 @@ def test_boxwhisker_simple_overlay(self): p1, p2 = plot.subplots.values() self.assertEqual(p1.handles['boxes'][0].get_path().vertices, p2.handles['boxes'][0].get_path().vertices) + + def test_box_whisker_padding_square(self): + curve = BoxWhisker([1, 2, 3]).options(padding=0.1) + plot = mpl_renderer.get_plot(curve) + y_range = plot.handles['axis'].get_ylim() + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) diff --git a/tests/plotting/matplotlib/testcurveplot.py b/tests/plotting/matplotlib/testcurveplot.py index 32af729903..3b0100a1c6 100644 --- a/tests/plotting/matplotlib/testcurveplot.py +++ b/tests/plotting/matplotlib/testcurveplot.py @@ -50,3 +50,97 @@ def test_curve_heterogeneous_datetime_types_with_pd_overlay(self): curve_pd = Curve((dates_pd, np.random.rand(10))) plot = mpl_renderer.get_plot(curve_dt*curve_dt64*curve_pd) self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735976.0)) + + def test_curve_padding_square(self): + curve = Curve([1, 2, 3]).options(padding=0.1) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_square_per_axis(self): + curve = Curve([1, 2, 3]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.4) + + def test_curve_padding_hard_xrange(self): + curve = Curve([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 3) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_soft_xrange(self): + curve = Curve([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 3) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_unequal(self): + curve = Curve([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_nonsquare(self): + curve = Curve([1, 2, 3]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_logx(self): + curve = Curve([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.89595845984076228) + self.assertEqual(x_range[1], 3.3483695221017129) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_logy(self): + curve = Curve([1, 2, 3]).options(padding=0.1, logy=True) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.89595845984076228) + self.assertEqual(y_range[1], 3.3483695221017129) + + def test_curve_padding_datetime_square(self): + curve = Curve([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.80000000005) + self.assertEqual(x_range[1], 736057.19999999995) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_datetime_nonsquare(self): + curve = Curve([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1, aspect=2 + ) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.90000000002) + self.assertEqual(x_range[1], 736057.09999999998) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) diff --git a/tests/plotting/matplotlib/testhistogramplot.py b/tests/plotting/matplotlib/testhistogramplot.py index 0e85f3ff52..63b8f3d652 100644 --- a/tests/plotting/matplotlib/testhistogramplot.py +++ b/tests/plotting/matplotlib/testhistogramplot.py @@ -2,13 +2,13 @@ import numpy as np -from holoviews.element import Dataset +from holoviews.element import Dataset, Histogram from holoviews.operation import histogram from .testplot import TestMPLPlot, mpl_renderer -class TestCurvePlot(TestMPLPlot): +class TestHistogramPlot(TestMPLPlot): def test_histogram_datetime64_plot(self): dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)]) @@ -17,4 +17,81 @@ def test_histogram_datetime64_plot(self): artist = plot.handles['artist'] ax = plot.handles['axis'] self.assertEqual(ax.get_xlim(), (736330.0, 736333.0)) - self.assertEqual([p.get_x() for p in artist.patches], [736330.0, 736330.75, 736331.5, 736332.25]) + bounds = [736330.0, 736330.75, 736331.5, 736332.25] + self.assertEqual([p.get_x() for p in artist.patches], bounds) + + def test_histogram_padding_square(self): + points = Histogram([(1, 2), (2, -1), (3, 3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.19999999999999996) + self.assertEqual(x_range[1], 3.8) + self.assertEqual(y_range[0], -1.4) + self.assertEqual(y_range[1], 3.4) + + def test_histogram_padding_square_positive(self): + points = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.19999999999999996) + self.assertEqual(x_range[1], 3.8) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_histogram_padding_square_negative(self): + points = Histogram([(1, -2), (2, -1), (3, -3)]).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.19999999999999996) + self.assertEqual(x_range[1], 3.8) + self.assertEqual(y_range[0], -3.2) + self.assertEqual(y_range[1], 0) + + def test_histogram_padding_nonsquare(self): + histogram = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(histogram) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.35) + self.assertEqual(x_range[1], 3.65) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_histogram_padding_logx(self): + histogram = Histogram([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = mpl_renderer.get_plot(histogram) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.41158562699652224) + self.assertEqual(x_range[1], 4.2518491541367327) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_histogram_padding_logy(self): + histogram = Histogram([(1, 2), (2, 1), (3, 3)]).options(padding=0.1, logy=True) + plot = mpl_renderer.get_plot(histogram) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.19999999999999996) + self.assertEqual(x_range[1], 3.8) + self.assertEqual(y_range[0], 1) + self.assertEqual(y_range[1], 3.3483695221017129) + + def test_histogram_padding_datetime_square(self): + histogram = Histogram([(np.datetime64('2016-04-0%d' % i, 'ns'), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = mpl_renderer.get_plot(histogram) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.19999999995) + self.assertEqual(x_range[1], 736057.80000000005) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_histogram_padding_datetime_nonsquare(self): + histogram = Histogram([(np.datetime64('2016-04-0%d' % i, 'ns'), i) for i in range(1, 4)]).options( + padding=0.1, aspect=2 + ) + plot = mpl_renderer.get_plot(histogram) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.34999999998) + self.assertEqual(x_range[1], 736057.65000000002) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) diff --git a/tests/plotting/matplotlib/testpointplot.py b/tests/plotting/matplotlib/testpointplot.py index 44a95e68a3..4984a7ac00 100644 --- a/tests/plotting/matplotlib/testpointplot.py +++ b/tests/plotting/matplotlib/testpointplot.py @@ -56,4 +56,97 @@ def test_points_rcparams_used(self): ax = plot.state.axes[0] lines = ax.get_xgridlines() self.assertEqual(lines[0].get_color(), 'red') + + def test_points_padding_square(self): + points = Points([1, 2, 3]).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_curve_padding_square_per_axis(self): + curve = Points([1, 2, 3]).options(padding=((0, 0.1), (0.1, 0.2))) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.4) + def test_points_padding_hard_xrange(self): + points = Points([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 3) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_soft_xrange(self): + points = Points([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 3) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_unequal(self): + points = Points([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_nonsquare(self): + points = Points([1, 2, 3]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_logx(self): + points = Points([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.89595845984076228) + self.assertEqual(x_range[1], 3.3483695221017129) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_logy(self): + points = Points([1, 2, 3]).options(padding=0.1, logy=True) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.89595845984076228) + self.assertEqual(y_range[1], 3.3483695221017129) + + def test_points_padding_datetime_square(self): + points = Points([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.80000000005) + self.assertEqual(x_range[1], 736057.19999999995) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + + def test_points_padding_datetime_nonsquare(self): + points = Points([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)]).options( + padding=0.1, aspect=2 + ) + plot = mpl_renderer.get_plot(points) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.90000000002) + self.assertEqual(x_range[1], 736057.09999999998) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) diff --git a/tests/plotting/matplotlib/testradialheatmap.py b/tests/plotting/matplotlib/testradialheatmap.py index a99342c5ce..b7949d70e4 100644 --- a/tests/plotting/matplotlib/testradialheatmap.py +++ b/tests/plotting/matplotlib/testradialheatmap.py @@ -46,7 +46,7 @@ def setUp(self): def test_get_data(self): plot = mpl_renderer.get_plot(self.element) - data, style, ticks = plot.get_data(self.element, {'z': (0, 3)}, {}) + data, style, ticks = plot.get_data(self.element, {'z': {'combined': (0, 3)}}, {}) wedges = data['annular'] for wedge, wdata in zip(wedges, self.wedge_data): self.assertEqual((wedge.center, wedge.width, wedge.r, @@ -56,7 +56,7 @@ def test_get_data(self): def test_get_data_xseparators(self): plot = mpl_renderer.get_plot(self.element.opts(plot=dict(xmarks=4))) - data, style, ticks = plot.get_data(self.element, {'z': (0, 3)}, {}) + data, style, ticks = plot.get_data(self.element, {'z': {'combined': (0, 3)}}, {}) xseparators = data['xseparator'] arrays = [np.array([[0., 0.25], [0., 0.5 ]]), @@ -66,7 +66,7 @@ def test_get_data_xseparators(self): def test_get_data_yseparators(self): plot = mpl_renderer.get_plot(self.element.opts(plot=dict(ymarks=4))) - data, style, ticks = plot.get_data(self.element, {'z': (0, 3)}, {}) + data, style, ticks = plot.get_data(self.element, {'z': {'combined': (0, 3)}}, {}) yseparators = data['yseparator'] for circle, r in zip(yseparators, [0.25, 0.375]): self.assertEqual(circle.radius, r) diff --git a/tests/plotting/matplotlib/testscatter3d.py b/tests/plotting/matplotlib/testscatter3d.py new file mode 100644 index 0000000000..ae9b28288c --- /dev/null +++ b/tests/plotting/matplotlib/testscatter3d.py @@ -0,0 +1,91 @@ +from holoviews.element import Scatter3D + +from .testplot import TestMPLPlot, mpl_renderer + + +class TestPointPlot(TestMPLPlot): + + def test_scatter3d_padding_square(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).options(padding=0.1) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 1.8) + self.assertEqual(z_range[1], 4.2) + + def test_curve_padding_square_per_axis(self): + curve = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).options(padding=((0, 0.1), (0.1, 0.2), (0.2, 0.3))) + plot = mpl_renderer.get_plot(curve) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.4) + self.assertEqual(z_range[0], 1.6) + self.assertEqual(z_range[1], 4.6) + + def test_scatter3d_padding_hard_zrange(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).redim.range(z=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 0) + self.assertEqual(z_range[1], 3) + + def test_scatter3d_padding_soft_zrange(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).redim.soft_range(z=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 0) + self.assertEqual(z_range[1], 4.2) + + def test_scatter3d_padding_unequal(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).options(padding=(0.05, 0.1, 0.2)) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 1.6) + self.assertEqual(z_range[1], 4.4) + + def test_scatter3d_padding_nonsquare(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.1) + self.assertEqual(x_range[1], 2.1) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 1.8) + self.assertEqual(z_range[1], 4.2) + + def test_scatter3d_padding_logz(self): + scatter3d = Scatter3D([(0, 1, 2), (1, 2, 3), (2, 3, 4)]).options(padding=0.1, logz=True) + plot = mpl_renderer.get_plot(scatter3d) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + z_range = plot.handles['axis'].get_zlim() + self.assertEqual(x_range[0], -0.2) + self.assertEqual(x_range[1], 2.2) + self.assertEqual(y_range[0], 0.8) + self.assertEqual(y_range[1], 3.2) + self.assertEqual(z_range[0], 1.8660659830736146) + self.assertEqual(z_range[1], 4.2870938501451725) + diff --git a/tests/plotting/matplotlib/testspikeplot.py b/tests/plotting/matplotlib/testspikeplot.py new file mode 100644 index 0000000000..e14872a5f5 --- /dev/null +++ b/tests/plotting/matplotlib/testspikeplot.py @@ -0,0 +1,88 @@ +import numpy as np + +from holoviews.element import Spikes + +from .testplot import TestMPLPlot, mpl_renderer + + +class TestSpikesPlot(TestMPLPlot): + + def test_spikes_padding_square(self): + spikes = Spikes([1, 2, 3]).options(padding=0.1) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + + def test_spikes_padding_square_heights(self): + spikes = Spikes([(1, 1), (2, 2), (3, 3)], vdims=['Height']).options(padding=0.1) + plot = mpl_renderer.get_plot(spikes) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 0.8) + self.assertEqual(x_range[1], 3.2) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_spikes_padding_hard_xrange(self): + spikes = Spikes([1, 2, 3]).redim.range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 3) + + def test_spikes_padding_soft_xrange(self): + spikes = Spikes([1, 2, 3]).redim.soft_range(x=(0, 3)).options(padding=0.1) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0) + self.assertEqual(x_range[1], 3.2) + + def test_spikes_padding_unequal(self): + spikes = Spikes([1, 2, 3]).options(padding=(0.05, 0.1)) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0.9) + self.assertEqual(x_range[1], 3.1) + + def test_spikes_padding_nonsquare(self): + spikes = Spikes([1, 2, 3]).options(padding=0.1, aspect=2) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0.9) + self.assertEqual(x_range[1], 3.1) + + def test_spikes_padding_logx(self): + spikes = Spikes([(1, 1), (2, 2), (3,3)]).options(padding=0.1, logx=True) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 0.89595845984076228) + self.assertEqual(x_range[1], 3.3483695221017129) + + def test_spikes_padding_datetime_square(self): + spikes = Spikes([np.datetime64('2016-04-0%d' % i) for i in range(1, 4)]).options( + padding=0.1 + ) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 736054.80000000005) + self.assertEqual(x_range[1], 736057.19999999995) + + def test_spikes_padding_datetime_square_heights(self): + spikes = Spikes([(np.datetime64('2016-04-0%d' % i), i) for i in range(1, 4)], vdims=['Height']).options( + padding=0.1 + ) + plot = mpl_renderer.get_plot(spikes) + x_range, y_range = plot.handles['axis'].get_xlim(), plot.handles['axis'].get_ylim() + self.assertEqual(x_range[0], 736054.80000000005) + self.assertEqual(x_range[1], 736057.19999999995) + self.assertEqual(y_range[0], 0) + self.assertEqual(y_range[1], 3.2) + + def test_spikes_padding_datetime_nonsquare(self): + spikes = Spikes([np.datetime64('2016-04-0%d' % i) for i in range(1, 4)]).options( + padding=0.1, aspect=2 + ) + plot = mpl_renderer.get_plot(spikes) + x_range = plot.handles['axis'].get_xlim() + self.assertEqual(x_range[0], 736054.90000000002) + self.assertEqual(x_range[1], 736057.09999999998) diff --git a/tests/plotting/testplotutils.py b/tests/plotting/testplotutils.py index 608e8ae371..bec300748d 100644 --- a/tests/plotting/testplotutils.py +++ b/tests/plotting/testplotutils.py @@ -15,7 +15,8 @@ from holoviews.plotting.util import ( compute_overlayable_zorders, get_min_distance, process_cmap, initialize_dynamic, split_dmap_overlay, _get_min_distance_numpy, - bokeh_palette_to_palette, mplcmap_to_palette, color_intervals) + bokeh_palette_to_palette, mplcmap_to_palette, color_intervals, + get_range, get_axis_padding) from holoviews.streams import PointerX try: @@ -612,6 +613,39 @@ def test_get_min_distance_int32_type_no_scipy(self): self.assertEqual(dist, 1.0) +class TestRangeUtilities(ComparisonTestCase): + + def test_get_axis_padding_scalar(self): + padding = get_axis_padding(0.1) + self.assertEqual(padding, (0.1, 0.1, 0.1)) + + def test_get_axis_padding_tuple(self): + padding = get_axis_padding((0.1, 0.2)) + self.assertEqual(padding, (0.1, 0.2, 0)) + + def test_get_axis_padding_tuple_3d(self): + padding = get_axis_padding((0.1, 0.2, 0.3)) + self.assertEqual(padding, (0.1, 0.2, 0.3)) + + def test_get_range_from_element(self): + dim = Dimension('y', soft_range=(0, 3), range=(0, 2)) + element = Scatter([1, 2, 3], vdims=dim) + drange, srange, hrange = get_range(element, {}, dim) + self.assertEqual(drange, (1, 3)) + self.assertEqual(srange, (0, 3)) + self.assertEqual(hrange, (0, 2)) + + def test_get_range_from_ranges(self): + dim = Dimension('y', soft_range=(0, 3), range=(0, 2)) + element = Scatter([1, 2, 3], vdims=dim) + ranges = {'y': {'soft': (-1, 4), 'hard': (-1, 3), 'data': (-0.5, 2.5)}} + drange, srange, hrange = get_range(element, ranges, dim) + self.assertEqual(drange, (-0.5, 2.5)) + self.assertEqual(srange, (-1, 4)) + self.assertEqual(hrange, (-1, 3)) + + + @attr(optional=1) # Flexx is optional class TestBokehUtils(ComparisonTestCase):